├── LICENSE ├── Programming ├── 代码本色:用编程模拟自然系统 │ └── README.md └── 深入浅出游戏编程模式 │ ├── README.md │ ├── img │ ├── 享元模式_1.png │ ├── 享元模式_2.png │ ├── 命令模式_1.png │ ├── 命令模式_2.png │ ├── 命令模式_3.png │ ├── 观察者模式_1.png │ ├── 观察者模式_2.png │ └── 观察者模式_3.png │ ├── 事件队列.md │ ├── 享元模式.md │ ├── 单例模式.md │ ├── 命令模式.md │ ├── 子类沙箱.md │ ├── 对象池.md │ ├── 数据局部性.md │ ├── 类型对象模式.md │ └── 观察者模式.md └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Programming/代码本色:用编程模拟自然系统/README.md: -------------------------------------------------------------------------------- 1 | # 代码本色:用编程模拟自然系统 2 | 3 | 本部分内容主要是 Voidmatrix 在阅读 Daniel Shiffman 所著的《The Nature of Code - Simulating Natural Systems with Processing》时记录的笔记,书中主要浅述了使用计算机对各类自然系统的模拟方法,并提供了基于 Processing 的示例代码,为了方便更多读者理解这部分笔记,示例代码将会被替换成在 [EtherAPI](https://github.com/VoidmatrixHeathcliff/EtherEngine) 环境下运行的 Lua 代码,[EtherAPI](https://github.com/VoidmatrixHeathcliff/EtherEngine) 对 Lua 的图形化开发提供了相当简洁的处理思路,~~欢迎大家关注加星~~ 4 | 5 | ## 目录 6 | 7 | + [随机](随机.md) 8 | + [向量](向量.md) 9 | + [力](力.md) 10 | + [振荡](振荡.md) 11 | + [粒子系统](粒子系统.md) 12 | + [物理函数库](物理函数库.md) 13 | + [自治智能体](自治智能体.md) 14 | + [细胞自动机](细胞自动机.md) 15 | + [分形](分形.md) 16 | + [代码的进化](代码的进化.md) 17 | + [神经网络](神经网络.md) 18 | 19 | ## 注意 20 | 21 | 部分配图可能来自网络或图书作者,对其进行其他用途前请先确认来源及版权要求 22 | -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/README.md: -------------------------------------------------------------------------------- 1 | # 深入浅出游戏编程模式 2 | 3 | 本部分内容主要是 Voidmatrix 在重复翻阅 Bob Nystrom 所著的《Game Programming Patterns》时产生的笔记,可能会掺杂一部分来自 Voidmatrix 本人的理解、反思以及代码 4 | 5 | ## 目录 6 | 7 | + 通用设计模式 8 | + [命令模式](命令模式.md) 9 | + [享元模式](享元模式.md) 10 | + [观察者模式](观察者模式.md) 11 | + [原型模式](原型模式.md) 12 | + [单例模式](单例模式.md) 13 | + [状态模式](状态模式.md) 14 | + 序列模式 15 | + [双缓冲模式](双缓冲模式.md) 16 | + [游戏循环](游戏循环.md) 17 | + [更新方法](更新方法.md) 18 | + 行为模式 19 | + [字节码](字节码.md) 20 | + [子类沙箱](子类沙箱.md) 21 | + [类型对象模式](类型对象模式.md) 22 | + 解耦模式 23 | + [组件模式](组件模式.md) 24 | + [事件队列](事件队列.md) 25 | + [服务定位器](服务定位器.md) 26 | + 优化模式 27 | + [数据局部性](数据局部性.md) 28 | + [脏标识](脏标识.md) 29 | + [对象池](对象池.md) 30 | + [空间分区](空间分区.md) 31 | 32 | ## 注意 33 | 34 | 部分配图可能来自网络或图书作者,对其进行其他用途前请先确认来源及版权要求 35 | -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/img/享元模式_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/img/享元模式_1.png -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/img/享元模式_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/img/享元模式_2.png -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/img/命令模式_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/img/命令模式_1.png -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/img/命令模式_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/img/命令模式_2.png -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/img/命令模式_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/img/命令模式_3.png -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/img/观察者模式_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/img/观察者模式_1.png -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/img/观察者模式_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/img/观察者模式_2.png -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/img/观察者模式_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/img/观察者模式_3.png -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/事件队列.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/事件队列.md -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/享元模式.md: -------------------------------------------------------------------------------- 1 | # 享元模式 2 | 3 | 简而言之,享元模式就是将对象的数据分为两部分,其中一部分没有特定指明是哪个对象的实例,因此可以在它们间共享,GoF 称这部分为 “固有状态”,一个很常见的例子便是开放世界中的树林的渲染和瓦片地图的数据存储方式 4 | 5 | 享元模式是对内存和开发者友好的,它可以通过减少重复部分数据避免内存冗余,并且通过合并类来减轻程序员的管理负担 6 | 7 | ## 巨大的森林 8 | 9 | 显而易见,在当下的技术水平来看,无论是多么细致逼真、制作精良的开放世界游戏,游戏中类似森林这种元素个体变化极多的存在,建模师都不会为每一个个体单独建立一个独立的模型,这不仅对建模师不公平,对程序而言也十分不友好 10 | 11 | 我们很容易想到,我们可以存储树的 “各部分”,然后通过程序将一棵棵相态各异的树 “拼凑” 成一个完整的对象,如果用代码表示,那么会得到这样的东西: 12 | 13 | ```c++ 14 | class Tree 15 | { 16 | private: 17 | Mesh mesh_; 18 | Texture bark_, leaves; 19 | Vector position_; 20 | double height_, thickness_; 21 | Color barkTint, leafTint; 22 | }; 23 | ``` 24 | 25 | 但是到此为止,我们还没有使用享元模式,部分到整体的思想不是享元模式的核心,享元模式的关键在于将这些拆分开的部分中的重复元素找出并且合并,所以我们就有了下面的代码: 26 | 27 | ```c++ 28 | class TreeModel 29 | { 30 | private: 31 | Mesh mesh_; 32 | Texture bark_, leaves; 33 | }; 34 | 35 | class Tree 36 | { 37 | private: 38 | TreeModel* model_; 39 | Vector position_; 40 | double height_, thickness_; 41 | Color barkTint, leafTint; 42 | }; 43 | ``` 44 | 45 | 也就是下图所展现的内容: 46 | 47 | ![享元模式_1.png](./img/享元模式_1.png) 48 | 49 | 这有点像 “[类型对象模式](./类型对象模式.md)”,两者都设计将一个类中的状态委托给其他类来存储或处理,来达到不同实例间共享状态的目的,但是两者的初衷并不相同,具体区别如下: 50 | 51 | + 使用类型对象的主要目的是将类型引入对象模型,减少定义类的数量,而共享内存则是水到渠成的好处 52 | 53 | + 享元模式则是从优化效率的角度出发,实例间共享数据并不是在设计享元类时应该首要考虑的方面 54 | 55 | 在真正调用图形接口进行渲染时,我们可以把共享的数据 `TreeModel` 只像 GPU 发送一次,而对每棵树独立的数据分别发送,现代的图形接口和显卡都支持这样的渲染方式,具体细节不再赘述 56 | 57 | Bob Nystrom 在《Game Programming Patterns》 中总结道: 58 | 59 | > 这个 API 是由显卡直接实现,意味着享元模式也许是唯一有硬件支持的 GoF 设计模式 60 | 61 | ## 地图数据 62 | 63 | 瓦片地图的使用在现代的游戏中也依然十分活跃,虽然它的表现不一定如某些老旧的地牢RPG游戏那样边界规整、分明,如 我的世界、泰拉瑞亚 等游戏就是瓦片地图的经典应用 64 | 65 | 和上述的森林渲染一样,我们自然也不会让每一个瓦片或每一类瓦片都占据一个独立的类,它们的大部分数据还是同样可以合并的 66 | 67 | 我们可以首先定义地形的枚举种类: 68 | 69 | ```c++ 70 | enum Terrain 71 | { 72 | TERRAIN_GRASS, 73 | TERRAIN_HILL, 74 | TERRAIN_RIVER 75 | // 其他地形…… 76 | }; 77 | ``` 78 | 79 | 然后将 “世界” 定义为巨大的网格: 80 | 81 | ```c++ 82 | class World 83 | { 84 | private: 85 | Terrain tiles_[WIDTH][HEIGHT]; 86 | } 87 | ``` 88 | 89 | 使用嵌套数组存储 2D 网格,在 C/C++ 这类语言中是很有效率的,因为它会将这些元素在内存中 “真正地” 打包到一起,这将有利于缓存命中,更多内容可以查看 “[数据局部性](./数据局部性.md)” 这一章节;但是在 Java 或其他内存管理语言中,底层的实现可能并不会是这样,但这并不是我们在此时应该关心的内容,我们只需要隐藏 2D 网格数据结构的实现细节,让代码尽可能保持简洁 90 | 91 | 这样,如果我们想获取玩家角色在不同地形上的移动开销,或判断某处地形是否是水域时,我们就可以这样写: 92 | 93 | ```c++ 94 | int World::getMovementCost(int x, int y) 95 | { 96 | switch (tiles_[x][y]) 97 | { 98 | case TERRAIN_GRASS: return 1; 99 | case TERRAIN_HILL: return 3; 100 | case TERRAIN_RIVER: return 2; 101 | // 其他地形…… 102 | } 103 | } 104 | 105 | bool World::isWater(int x, int y) 106 | { 107 | switch (tiles_[x][y]) 108 | { 109 | case TERRAIN_GRASS: return false; 110 | case TERRAIN_HILL: return false; 111 | case TERRAIN_RIVER: return true; 112 | // 其他地形…… 113 | } 114 | } 115 | ``` 116 | 117 | 但是,这样的设计并不是优雅的,移动开销和水域标识本应是地图区块的数据,但是我们却通过外部的方法将这些数据拆散开了,这违背了我们设计对象的初衷 118 | 119 | 所以我们就可以将上述原本属于 `World` 这个类的方法拆分合并到 `Terrain` 类的属性和方法中,代码如下: 120 | 121 | ```c++ 122 | class Terrain 123 | { 124 | public: 125 | Terrain(int movementCost, bool isWater, Texture texture) 126 | : movementCost_(movementCost), isWater_(isWater), texture_(texture) {} 127 | int getMovement() const { return movementCost_; } 128 | bool isWater() const {return isWater_; } 129 | const Texture& getTexture() const { return texture_; } 130 | 131 | private: 132 | int movementCost_; 133 | bool isWater_; 134 | Texture texture_; 135 | } 136 | ``` 137 | 138 | 你可能注意到了,这里我们多处使用了 `const` 来进行限定,这是为了防止对这些多处共享的对象错误地修改而造成违背开发者意愿的事情发生 139 | 140 | 但是我们可能对这种代码处理方式还是不太满意,因为我们将应用的显示行为(如获取移动开销和水域判定这些逻辑行为)绑定到了类内部,从而让享元对象变为几乎不可改变的状态,似乎有些优化过度了;但是我们也不想为每个区块都在内存中保存一个实例——如果仔细观察就可以发现,其实地形无非就寥寥几种,而整张地图无非就是保存每一种区块出现在何处: 141 | 142 | > 用享元的术语讲,区块的所有状态都是 “固有的” 或者说 “上下文无关的” 143 | 144 | 鉴于此,我们就没必要开辟多块内存分别保存多个同种地形类型的数据,地面上的草区块享元数据两两无异,所以我们就可以用 `Terrain` 对象指针组成网格: 145 | 146 | ```c++ 147 | class World 148 | { 149 | private: 150 | Terrain* tiles_[WIDTH][HEIGHT]; 151 | // 其余代码…… 152 | } 153 | ``` 154 | 155 | 这时的世界地图就变成了下面这幅样子: 156 | 157 | ![享元模式_2.png](./img/享元模式_2.png) 158 | 159 | 由于地形实例可能在游戏中会多处使用,如果想要动态分配,它们的生命周期可能就会比较复杂,所以,我们可以直接在游戏世界中存储它们: 160 | 161 | ```c++ 162 | class World 163 | { 164 | public: 165 | World() 166 | : grassTerrain_(1, false, GRASS_TEXTURE), 167 | hillTerrain_(3, false, HILL_TEXTURE), 168 | riverTerrain_(2, true, RIVER_TEXTURE) 169 | {} 170 | 171 | private: 172 | Terrain grassTerrain_; 173 | Terrain hillTerrain_; 174 | Terrain riverTerrain_; 175 | } 176 | ``` 177 | 178 | 这样,我们就可以通过下面的代码生成地图: 179 | 180 | ```c++ 181 | void World::generateTerrain() 182 | { 183 | // 生成山地和草地地形 184 | for (int x = 0; x < WIDTHl x++) 185 | for (int y = 0; y < HEIGHT; y++) 186 | if (random(10) == 0) 187 | tiles_[x][y] = &hillTerrain_; 188 | else 189 | tiles_[x][y] = &grassTerrain_; 190 | // 生成河流 191 | int x = random(WIDTH); 192 | for (int y = 0; y < HEIGHT; y++) 193 | tiles_[x][y] = &riverTerrain_; 194 | } 195 | ``` 196 | 197 | 我们现在不需要在 `World` 中通过方法接触地形属性,而是可以直接暴露出 `Terrain` 对象: 198 | 199 | ```c++ 200 | const Terrain& World::getTile(int x, int y) const 201 | { 202 | return *tiles_[x][y]; 203 | } 204 | ``` 205 | 206 | 使用这种方式,`World` 就不再与各种地形细节耦合,如果想获取某一区块的属性,就可以直接从对应区块对象中获得: 207 | 208 | ```c++ 209 | World world; 210 | world.generateTerrain(); 211 | int cost = world.getTile(2, 3).getMovementCost(); 212 | ``` 213 | 214 | 我们现在又回到了直接操作实体对象的 API,但是由于指针通常情况下是远小于枚举类型的,所以这样的操作性能更优,几乎没有任何额外开销 215 | 216 | ## 拓展 217 | 218 | + 在实际游戏开发过程中,我们一般不会在游戏一开始就创建好所有的享元,这样的设计在多数情况下是很丑陋的(如游戏中分为多个世界,而每个世界中的地形享元都不同,在当前世界中加载其他世界的享元到内存中就成了额外的负担);如果我们不能预料什么时候才会真正需要这些享元,那么一个显而易见的策略便是在需要的时候才去创建它;同时,为了保持共享的优势,我们在尝试创建一个享元的时候,必须先判断是否已经存在了一个相同的实例,这听起来像 “[单例模式](./单例模式.md)”,但是如果我们将构造函数封装在查询对象是否存在的接口之后,使用 “工厂方法” 会是更好的选择 219 | 220 | + 为了返回一个早些时候创建的享元对象,我们可能需要追踪已经实例化的全部享元对象,这时,“[对象池](./对象池.md)” 便派上用场了 221 | 222 | + 在使用 “[状态模式](./状态模式.md)” 时,经常会出现一些没有任何特定字段的 “状态对象”,但这个状态的标识和方法都很有用,在这种情况下,享元模式就可以在不同的状态机上使用相同的对象实例 -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/单例模式.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/单例模式.md -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/命令模式.md: -------------------------------------------------------------------------------- 1 | # 命令模式 2 | 3 | GoF 中的定义: 4 | 5 | > 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。 6 | 7 | > 命令模式是一种回调的面向对象实现。 8 | 9 | 简而言之,便是将 “操作” 封装为类,将其实例化为对象后作为 “第一公民” 对待,一个通俗易懂的应用便是如绘图软件中的 `Ctrl+Z` 撤销功能 10 | 11 | ## 角色按键控制 12 | 13 | 以手柄为例,四个按键 `X Y B A` 分别对应角色的 `Jump FireGun Lurch SwapWeapon` 四个行为,如下图所示: 14 | 15 | ![命令模式_1.png](./img/命令模式_1.png) 16 | 17 | 那么,可以轻松想到的代码便是下面这种: 18 | 19 | ```c++ 20 | class InputHandler 21 | { 22 | public: 23 | void handleInput(); 24 | }; 25 | 26 | void InputHandler::handleInput() 27 | { 28 | if (isPressed(BUTTON_X)) jump(); 29 | else if (isPressed(BUTTON_Y)) fireGun(); 30 | else if (isPressed(BUTTON_A)) swapWeapon(); 31 | else if (isPressed(BUTTON_B)) lurchIneffectively(); 32 | } 33 | ``` 34 | 35 | 虽然这样的代码可以完成功能,但是由于按键响应是硬编码的,所以无法做到根据玩家的习惯或意愿在游戏的 “设置” 中修改按键键位,那么,最简单的命令模式便出现了,我们首先定义一个用来描述 “命令” 的基类,来代表可触发的游戏行为: 36 | 37 | ```c++ 38 | class Command 39 | { 40 | public: 41 | virtual ~Command() {} 42 | virtual void execute() = 0; 43 | }; 44 | ``` 45 | 46 | 然后将刚才直接调用的指令函数封装到类中,便是下面这种代码: 47 | 48 | ```c++ 49 | class JumpCommand : public Command 50 | { 51 | public: 52 | virtual void execute() { jump(); } 53 | }; 54 | 55 | class FireCommand : public Command 56 | { 57 | public: 58 | virtual void execute() { fireGun(); }; 59 | }; 60 | 61 | // 其余指令代码不再赘述 62 | ``` 63 | 64 | 然后在刚刚剥离出去的 `InputHandler` 输入处理类中,只需要为每个按键存储一个指向指令的指针: 65 | 66 | ```c++ 67 | class InputHandler 68 | { 69 | public: 70 | void handleInput(); 71 | 72 | private: 73 | Command* buttonX_; 74 | Command* buttonY_; 75 | Command* buttonA_; 76 | Command* buttonB_; 77 | }; 78 | 79 | void InputHandler::handleInput() 80 | { 81 | if (isPressed(BUTTON_X)) buttonX_->execute(); 82 | else if (isPressed(BUTTON_Y)) buttonY_->execute(); 83 | else if (isPressed(BUTTON_A)) buttonA_->execute(); 84 | else if (isPressed(BUTTON_B)) buttonB_->execute(); 85 | } 86 | ``` 87 | 88 | 现在,在每次按键后,会通过一层间接寻址调用函数,示意图如下: 89 | 90 | ![命令模式_2.png](./img/命令模式_2.png) 91 | 92 | 注意,这里的代码并没有对 `NULL` 进行单独检测,而在实际的游戏开发过程中,可能并不是每一个手柄上或键盘上的按键都对应着一个命令,这时我们便可以定义一个单独的空命令类继承自命令基类,而它的执行函数不做任何事情,这样就可以不需要为每一个键设置为 `NULL`,而只需要指向这个类,如下方的代码: 93 | 94 | ```c++ 95 | class EmptyCommand : public Command 96 | { 97 | public: 98 | virtual void execute() {}; 99 | } 100 | ``` 101 | 102 | 这种使用方式同样为一种设计模式,它的名字便是 “空对象模式” 103 | 104 | ## 解耦角色和命令 105 | 106 | 在上述内容中,我们做了一个十分有局限性的假设,那就是像 `jump() fireGun()` 等函数可以找到玩家角色,获取并修改玩家数据,就像操控木偶一样操控玩家对象,这样不仅对代码有了更多限制,而且如果想要让玩家控制不同的游戏角色,这也是无法轻易实现的。 107 | 108 | 如果我们把玩家所控制的角色变为指令执行时的参数传入,而不是让函数去寻找它们控制的角色那么上述两个问题便都可以轻而易举地解决: 109 | 110 | ```c++ 111 | class Command 112 | { 113 | public: 114 | virtual ~Command() {} 115 | virtual void execute(GameActor& actor) = 0; 116 | }; 117 | 118 | class JumpCommand : public Command 119 | { 120 | public: 121 | virtual void execute(GameActor& actor) { actor.jump(); } 122 | }; 123 | 124 | class FireCommand : public Command 125 | { 126 | public: 127 | virtual void execute(GameActor& actor) { actor.fireGun(); }; 128 | }; 129 | 130 | // 其余指令代码不再赘述 131 | ``` 132 | 133 | 到目前为止我们只修改了 `Command` 类,在原先代码中真正调用 `execute()` 函数的 `InputHandler` 类同样需要修改。 134 | 135 | 我们在这里使用 “延迟调用”,不将玩家作为参数传入后在 `handleInput()` 函数中自动调用,而是将它延迟到外部手动调用,这样它在调用时才会知晓自己作用在哪个角色上: 136 | 137 | ```c++ 138 | class InputHandler 139 | { 140 | public: 141 | Command* handleInput(); // 只需要修改此处函数 142 | 143 | private: 144 | Command* buttonX_; 145 | Command* buttonY_; 146 | Command* buttonA_; 147 | Command* buttonB_; 148 | }; 149 | 150 | Command* InputHandler::handleInput() 151 | { 152 | if (isPressed(BUTTON_X)) return buttonX_; 153 | else if (isPressed(BUTTON_Y)) return buttonY_; 154 | else if (isPressed(BUTTON_A)) return buttonA_; 155 | else if (isPressed(BUTTON_B)) return buttonB_; 156 | 157 | return NULL; // 如果没有按键被按下,则不进行任何处理 158 | } 159 | ``` 160 | 161 | 我们在实际调用时只需要这样写: 162 | 163 | ```c++ 164 | GameActor actor; 165 | InputHandler inputHandler; 166 | Command* command = inputHandler.handleInput(); 167 | if (command) command->execute(actor); 168 | ``` 169 | 170 | 有了这样的代码,我们便可以让玩家轻松控制不同的角色而不需要为每个角色单独编写按键响应代码;同样,如果我们想让AI托管玩家或为游戏中新增和玩家行为相似的AI,只需要让AI生成 `Command` 对象便可以做到 171 | 172 | 如果我们把这些指令序列化,通过网络传输到另一台机器上,再反序列化重现出来,这样我们就可以实现网络多人游戏的基础部分了 173 | 174 | ## 撤销和重做 175 | 176 | 这个例子可能是命令模式最广为人知的使用场景了 177 | 178 | 如果一个命令对象可以做一件事,那么它也一定可以撤销这件事——这在策略类游戏中很常见(如悔棋等),或者在各类编辑器中,回滚使用者的操作(如PS或各类文本编辑器) 179 | 180 | 在游戏中,移动某个角色的命令类代码可能是下面这样: 181 | 182 | ```c++ 183 | class MoveUnitCommand : public Command 184 | { 185 | public: 186 | MoveUnitCommand(Unit* unit, int x, int y) 187 | : unit_(unit), x_(x), y_(y) {} 188 | virtual void execute() { unit->moveTo(x_, y_); } 189 | 190 | private: 191 | Unit* unit_; 192 | int x_, y_; 193 | }; 194 | ``` 195 | 196 | 我们可以看出,这里使用的命令与前面所说的有所不同,我们将被修改的角色通过构造函数传入到了指令对象中,并且保存在了指令对象内,而不是如之前所说的通用移动命令,这样的移动命令代表 “某回合中某一角色的特定移动”,这在晚些时候讲述的撤销中就显得有用起来了 197 | 198 | 对应的按键处理函数可能是下面这样: 199 | 200 | ```c++ 201 | Command* handleInput() 202 | { 203 | Unit* unit = getSelectedUnit(); // 首先调用函数获取需要修改的对象 204 | 205 | if (isPressed(BUTTON_UP)) 206 | return new MoveUnitCommand(unit, unit->x(), unit->y() - 1); 207 | 208 | if (isPressed(BUTTON_DOWN)) 209 | return new MoveUnitCommand(unit, unit->x(), unit->y() + 1); 210 | 211 | // 其余移动代码不再赘述 212 | 213 | return NULL; 214 | } 215 | ``` 216 | 217 | 注意像 C++ 这种需要手动垃圾管理的语言,接收并执行上述函数返回的命令的代码也需要负责释放对象内存 218 | 219 | 接下来我们为这些类稍作修改,让他们优雅地支持撤销功能: 220 | 221 | 首先应该修改的是 `Command` 基类,新增 `undo()` 函数: 222 | 223 | ```c++ 224 | class Command() 225 | { 226 | public: 227 | virtual ~Command() {} 228 | virtual void execute() = 0; 229 | virtual void undo() = 0; // 新增撤销函数 230 | }; 231 | ``` 232 | 233 | 然后在 `MoveUnitCommand` 移动命令类中实现撤销逻辑,由于需要回到之前的状态,所以我们也需要保存角色移动前的位置信息: 234 | 235 | ```c++ 236 | class MoveUnitCommand : public Command 237 | { 238 | public: 239 | MoveUnitCommand(Unit* unit, int x, int y) 240 | : unit_(unit), xBefore_(0), yBefore_(0), x_(x), y_(y) {} 241 | virtual void execute() 242 | { 243 | xBefore_ = unit_->x(), yBefore_ = unit_->y(); // 保存移动前的角色位置信息 244 | unit->moveTo(x_, y_); 245 | } 246 | 247 | virtual void undo() { unit_->moveTo(xBefore_, yBefore_); } 248 | 249 | private: 250 | Unit* unit_; 251 | int xBefore_, yBefore_, x_, y_; 252 | } 253 | ``` 254 | 255 | 这看起来像 “备忘录” 模式,不过命令趋向于储存、修改和恢复对象的某一小部分(如移动命令只关心角色单位的位置信息),而不是对角色对象的全部状态储存快照,这种定制化存储修改的设计在内存上消耗更少 256 | 257 | 想要支持多重撤销也很容易实现,在多数支持撤销的编辑器中,多重撤销是最基本的功能之一:我们只需要记录指令列表,然后使用指针记录 “当前” 指令,当新的指令出现时,我们将其添加到列表尾部,并且将 “当前” 指针指向它;而撤销和重做就可以实现为指针的后退和前进,示意图如下: 258 | 259 | ![命令模式_3.png](./img/命令模式_3.png) 260 | 261 | 需要注意的是,如果用户在撤销后向列表中添加了新的命令,那么则需要先清除 “当前” 指针所指命令后的所有命令,再将新的命令添加进去,配合日常的使用经验很容易理解 262 | 263 | 重做在游戏中并不常见,但是重放很常见,一种简单的重放便是记录游戏每帧的状态,但是这样会消耗大量的内存;如果游戏引擎只记录每个实体每帧运行的命令(甚至记录间隔不需要像 “每帧” 这样频繁),重放时只需要正常运行一遍所记录的命令,便可以实现内存消耗相对更优的回放,推测王者荣耀中的游戏回放可能使用了此模式 264 | 265 | ## 用类还是函数? 266 | 267 | 上述代码中使用类来编写的主要原因是 C++ 对第一公民函数的支持十分有限,函数指针没有状态,所以需要定义类来存储各种状态 268 | 269 | Bob Nystrom 在《Game Programming Patterns》 中写道: 270 | 271 | > 命令模式是为一些没有闭包(closure)的语言模拟闭包 272 | 273 | 闭包是自动包装状态的完美解决方案,但是它们由于过于自动化而很难看清包装的真正状态有哪些,所以有时定义一个有字段的真实类更能帮助读者理解命令关心哪些数据 274 | 275 | 以 JavaScript 为例,前面所述的移动命令代码便可以写成: 276 | 277 | ```javascript 278 | function makeMoveUnitCommand(unit, x, y) { 279 | var xBefore, yBefore; 280 | return { 281 | execute: function() { 282 | xBefore = unit.x(); 283 | yBefore = unit.y(); 284 | unit.moveTo(x, y); 285 | }, 286 | undo: function() { 287 | unit.moveTo(xBefore, yBefore); 288 | } 289 | } 290 | } 291 | ``` 292 | 293 | ## 拓展 294 | 295 | + 在游戏开发过程中,可能会定义许多不同的命令类,为了更容易实现这些类,我们会定义一个具体的基类,包含一些能定义行为的更高层方法,这将命令的主体 `execute()` 转移到了 “[子类沙箱](./子类沙箱.md)” 中 296 | 297 | + 上述的例子中,我们明确指定了具体哪个角色会处理命令,而在某些对象模型分层复杂的情况下,我们可能需要让对象响应命令并将它传递给自己的下属对象,这就是 “职责链模式” 298 | 299 | + 某些命令可能是无状态的纯粹行为,这种情况下为每个命令都生成对象就有些浪费内存了,某些命令的实例可能是等价的,所以我们就可以用 “[享元模式](./享元模式.md)” 来优化这件事 300 | 301 | + 或许你也可以用万恶的 “[单例模式](./单例模式.md)” 来实现它 -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/子类沙箱.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/子类沙箱.md -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/对象池.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/对象池.md -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/数据局部性.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/数据局部性.md -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/类型对象模式.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoidmatrixHeathcliff/GameDeveloperNotes/ab55263ba6040af6544a3c9c30dab0a2b317dca2/Programming/深入浅出游戏编程模式/类型对象模式.md -------------------------------------------------------------------------------- /Programming/深入浅出游戏编程模式/观察者模式.md: -------------------------------------------------------------------------------- 1 | # 观察者模式 2 | 3 | 观察者模式可能是应用最为广泛的设计模式了,Java 将它放到了自己的核心库 `java.util.Observer` 中,C# 更是将其通过 `event` 关键字把它嵌入到了自己的语法中 4 | 5 | 一种常见的定义可以把它描述为: 6 | 7 | > 一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统 8 | 9 | 在游戏中最常见的用途便是 “成就系统” 10 | 11 | ## 成就系统 12 | 13 | 在游戏中,成就系统存储了玩家完成的各种各样的挑战,譬如 “通过穿模让自己从桥面上掉下去”(虽然一般来说,严谨的游戏开发者都不会在游戏的最终发布版本中设计此类成就),也就是说,在处理 “从桥上掉落” 这个物理引擎相关的代码部分,必须要对诸如 `unlockFallOffBridge()` 的方法进行调用,那么,成就系统便和物理系统紧密地耦合到了一起,而诸如 “在当前关卡回档 1000 次” 这类成就,可能又会和存档系统进行交互,代码又会交织成一团乱麻…… 14 | 15 | 那么,我们该如何解耦成就系统和其他它所关心的系统之间的关系呢? 16 | 17 | 观察者模式就是处理这类棘手问题的专家! 18 | 19 | 例如,一个常见的物理系统可能会处理重力,追踪哪些游戏对象待在地表,哪些坠入深渊,为了实现上述的桥面掉落成就,我们可以这样做: 20 | 21 | ```c++ 22 | void Physics::updateEntity(Entity& entity) 23 | { 24 | bool wasOnSurface = entity.isOnSurface(); 25 | entity.accelerate(GRAVITY); 26 | entity.update(); 27 | if (wasOnSurface && !entity.isOnSurface()) 28 | // 如果更新之前位于桥面而更新后不在桥面上,则将 “开始坠落” 事件广播出去 29 | notify(entity, EVENT_START_FALL); 30 | } 31 | ``` 32 | 33 | 这段代码所做的事情就仿佛在说,“额,我不知道谁对这件事感兴趣,但是刚才那个东西掉下去了,你们可以做自己想做的事情” 34 | 35 | 物理引擎虽然确实决定了要发送什么通知,但是这并没有完全解耦:成就系统需要检查这个正在下落的游戏对象时主角还是主角射出的弓箭,还需要检查在这之前主角有没有过类似的经历(毕竟没有人希望自己在每次掉下桥时都能收到礼花和炫光相伴的成就解锁),这些代码都无需牵扯物理引擎 36 | 37 | 仔细思考一下,`notify()` 函数的具体实现似乎是一个到现在为止还没有解决的麻烦,如果我们更新了成就系统,或者坠落下桥的成就系统不再关心这件事,那么我们就可能需要动态那些正在关心的听众们的列表,所以这里就对灵活性提出了更高的要求 38 | 39 | ## 如何实现? 40 | 41 | Talk is cheap,如果我们还不知道如何设计消息的发布者,那么就先从那些收听者们这里进行编码: 42 | 43 | ```c++ 44 | class Observer 45 | { 46 | public: 47 | virtual ~Observer() {} 48 | virtual void onNotify(const Entity& entity, Event event) = 0; 49 | }; 50 | ``` 51 | 52 | `onNotify()` 函数所需要的参数取决于具体的设计,典型的参数便是代码中所描述的:发送通知的对象和一个装入其他数据细节的参数;泛型或模板编程在这里可能十分有用,在这里为了方便起见,参数被硬编码为一个游戏实体和一个描述发生了什么事件的枚举 53 | 54 | 然后,成就系统我们就可以通过继承观察者类来实现: 55 | 56 | ```c++ 57 | class Achievements : public Observer 58 | { 59 | public: 60 | virtual void onNotify(const Entity& entity, Event event) 61 | { 62 | switch (event) 63 | { 64 | case EVENT_ENTITY_FALL: 65 | if (entity.isHero() && heroIsOnBridge_) 66 | unlock(ACHIEVEMENT_FELL_OFF_BRIDGE); // 解锁成就 67 | break; 68 | 69 | // 处理其他事件的代码,更新 heroIsOnBridge_ 变量…… 70 | } 71 | } 72 | 73 | private: 74 | void unlock(Achievement achievement) 75 | { 76 | // 判断成就是否已经解锁并尝试解锁成就 77 | } 78 | 79 | bool heroIsOnBridge_; 80 | }; 81 | ``` 82 | 83 | 被观察的对象拥有通知的方法函数,在 GoF 中,这些对象被称为 “主题”,主题会有一个列表,用来存储等它通知的观察者: 84 | 85 | ```c++ 86 | class Subject 87 | { 88 | private: 89 | Observer* observers_[MAX_OBSERVERS]; 90 | int numObservers_; 91 | }; 92 | ``` 93 | 94 | 在实际开发中,这里的 `observer_` 使用动态集合效果会更好(接下来的代码会说明这点),这里使用定长数组方便不习惯 C++ 标准库的读者们理解 95 | 96 | 关键在于主题暴露了公开的 API 来修改这个列表: 97 | 98 | ```c++ 99 | class Subject 100 | { 101 | public: 102 | void addObserver(Observer* observer) 103 | { 104 | // 将新的观察者添加到数组中…… 105 | } 106 | 107 | void removeObserver(Observer* observer) 108 | { 109 | // 将观察者从数组中移除 110 | } 111 | 112 | // 其他代码…… 113 | }; 114 | ``` 115 | 116 | 到现在为止,这就允许了外界代码控制谁接收通知,被观察者通过主题与观察者交流,但是不相互耦合,在这个示例中,没有一行物理代码会提及成就,但是二者仍可以相互交流,这就是观察者模式的精髓之处 117 | 118 | 被观察者使用列表而不是单一变量的设计也是十分重要的,这就允许了多个观察者(可能涉及多个系统)同时对于一个目标主题做出反应,譬如音效系统可能也需要在玩家坠落桥底的时候播放合适的音乐,而单一变量则会将后面添加的观察者顶替先前观察者的位置 119 | 120 | 现在,被观察者(主题)的剩余任务就是发送通知了: 121 | 122 | ```c++ 123 | class Subject 124 | { 125 | protected: 126 | void notify(const Entity& entity, Event event) 127 | { 128 | for (int i = 0; i < numObservers_; i++) 129 | observers_[i]->onNotify(entity, event); 130 | } 131 | }; 132 | ``` 133 | 134 | 注意,上面的示例假设了观察者不会在自己的 `onNotify()` 方法中修改观察者列表和观察者对象,如果观察者希望能够在收到通知时修改游戏对象的数据,那么开发者可能需要关注当前观察者列表中存在的观察者会不会对通知的顺序敏感,有时不同的通知顺序在同一个实体被修改时会产生不同的结果,毕竟看似同时通知的设计具体是通过数组的遍历先后通知实现的 135 | 136 | 回到物理系统上,我们只需要给物理系统添加挂钩让它可以发送消息,成就系统便可以和这一部分连线来接收消息,按照传统的设计模式方法实现,通过继承: 137 | 138 | ```c++ 139 | class Physics : public Subject 140 | { 141 | public: 142 | void updateEntity(Entity& entity); 143 | }; 144 | ``` 145 | 146 | 注意,在先前的代码里面,我们将 `notify()` 实现为了 `Subject` 内的保护方法,这样派生的物理系统便可以调用并发出通知,但是外部代码不可以;同时,`addObserver()` 和 `removeObserver()` 是公开的,所以任何能够接触物理引擎的东西都可以观察它 147 | 148 | 在实际开发过程中,我们会避免在这里使用继承,让 `Physics` 拥有一个 `Subject` 实例,而不是直接观察物理系统本身,例如被观察的是独立的 “下落事件” 的对象,观察者可以这样注册自己: 149 | 150 | ```c++ 151 | physics.entityFell().addObserver(this); 152 | ``` 153 | 154 | 关于 “观察者” 系统和 “事件” 系统的不同之处,Bob Nystrom 在《Game Programming Patterns》 中写道: 155 | 156 | > 对我而言,这是 “观察者” 与 “事件” 系统的不同之处。使用前者,你观察做了有趣事情的事物。使用后者,你观察的对象代表了发生的有趣事情 157 | 158 | 现在,当物理系统做了值得被关注的事情,它便会调用 `notify()`,就像前面代码所展示的,它遍历了观察者列表,通知了所有观察者 159 | 160 | ![观察者模式_1.png](./img/观察者模式_1.png) 161 | 162 | 简单来说,只需要一个类管理一个列表指向接口实例的指针 163 | 164 | > 很简单,对吧?难以置信的是,如此直观的东西是无数程序和应用框架交流的主心骨 165 | 166 | ## 观察者模式的缺陷? 167 | 168 | ### 太慢了…… 169 | 170 | 设计模式似乎总是被诟病效率太低,而观察者模式的名声似乎格外坏,因为一些名声不好的东西和他如影随形,譬如 “事件”,“消息”,甚至是 “数据绑定” 171 | 172 | 计算机科学理论中很重要的一点便是 “权衡”,一个很简单的例子便是在算法领域设计时所考虑的时间和空间之间的权衡,在宏观的开发过程中,代码的表达能力和可维护性与程序的效率在一定程度上也是存在制约关系的 173 | 174 | 其实有些时候并不是狂热的性能追求者们所想的那样,以上述为例,发送通知知识简单地遍历列表,调用一些虚方法,虽然这确实会比静态调用慢一点,但是使用这个模式可以为乱如麻的系统交互代码解耦,为后续的维护迭代创造非常清爽的后路,这一点性能的牺牲完全是可以忽略的 175 | 176 | ### 太快了? 177 | 178 | 在上述代码设计中,我们注意到一个问题:被观察者直接调用了观察者,这就意味着只有在所有观察者的通知方法返回后,被观察者才会继续完成自己的工作;简而言之,那便是 “观察者会阻塞被观察者的运行” 179 | 180 | 有经验的读者可能很快想到了使用多线程或事件同步,是的,无论如何,我们都是想要让它尽可能快地返回,这样才不会让整个游戏程序卡在这里 181 | 182 | 但是,必须要小心在观察者使用线程和锁,如果观察者试图获得被观察者的锁,游戏就会进入到死锁中,在多线程引擎中,使用 “[事件队列](./事件队列.md)” 来进行异步通信是一个很不错的选择 183 | 184 | ### 太多的动态分配! 185 | 186 | 确实,正如之前所说的,在实际的开发中,观察者列表的长度通常是动态修改的,即使是在有垃圾回收机制的语言中,内存分配和回收同样需要时间,即使这些工作都是自动进行的,另外,内存分页也是问题 187 | 188 | 使用 “[对象池](./对象池.md)” 或许是一个不错的思路,又或者我们可以尽可能地在游戏一开始就加入所有观察者而不乱动它们,除此之外,我们或许还可以这样做: 189 | 190 | + 链式观察者 191 | 192 | 在上述的代码中,`Subject` 只是拥有了一列表指针指向观察它的 `Observer` ,而 `Observer` 本身并没有对这个列表进行引用,它是纯粹的虚接口,而没有使用有状态的类 193 | 194 | 但是如果我们在 `Observer` 中存放一些状态,那么我们就可以将观察者列表分布到观察者自己身上来解决动态分布的问题:简单来说,就是不再让主题(被观察者)保留指针列表,而是让观察者对象本身成为链表的一部分 195 | 196 | ![观察者模式_2.png](./img/观察者模式_2.png) 197 | 198 | 为了实现这一点,`Subject` 中就不能再使用数组,而是保存链表的头部指针: 199 | 200 | ```c++ 201 | class Subject 202 | { 203 | public: 204 | Subject() : head_(NULL) {} 205 | 206 | // 其他代码…… 207 | 208 | private: 209 | Observer* head_; 210 | }; 211 | ``` 212 | 213 | 然后,我们需要继续修改 `Observer` 类,让它拥有指向链表下一观察者节点的指针: 214 | 215 | ```c++ 216 | class Observer 217 | { 218 | friend class Subject; 219 | 220 | public: 221 | Observer : next_(NULL) {} 222 | 223 | // 其他代码…… 224 | 225 | private: 226 | Observer* next_; 227 | } 228 | ``` 229 | 230 | 一个小细节,我们让 `Subject` 成为了友类,这样被观察者就拥有了增删观察者的 API;注册一个观察者到链表中,我们使用最简单的方式来实现,直接让它插入到链表的头部: 231 | 232 | ```c++ 233 | void Subject::addObserver(Observer* observer) 234 | { 235 | observer->next_ = head_; 236 | head_ = observer; 237 | } 238 | ``` 239 | 240 | 另一种选择就是让观察者添加到链表的尾部,这样我们就需要保存一个单独的 `tail_` 指针指向链表的最后一个节点或者遍历整条链表;正像我们前面所说的,当前观察者链表中存在的观察者不应该对通知的顺序敏感,如果能够保证做到这一点,那么直接将其添加到链表头部会是更简明的策略 241 | 242 | 下一步是完成观察者的移除操作: 243 | 244 | ```c++ 245 | void Subject::removeObserver(Observer* observer) 246 | { 247 | if (head == observer) 248 | { 249 | head_ = observer->next; 250 | observer->next = NULL; 251 | return; 252 | } 253 | 254 | Observer* current = head_; 255 | while (current != NULL) 256 | { 257 | if (current->next_ == observer) 258 | { 259 | current->next_ = observer->next_; 260 | observer->next_ = NULL; 261 | return; 262 | } 263 | 264 | current = current->next_; 265 | } 266 | } 267 | ``` 268 | 269 | 本质还是对链表节点的添加和删除操作,相信熟悉链表这一数据结构的读者可以很轻松理解;当然,删除节点时可以使用指向指针的指针实现更优雅的操作来应对头结点的情况,但是为了示例代码的简明起见,还是使用丑陋的 `if` 进行特殊情况处理 270 | 271 | 另外,在实际的项目中,通常会使用双向链表而不是单向,这样我们只需要常量时间便可以移除一个观察者 272 | 273 | 剩下的事情就是发送通知了,这和遍历数组一样简单: 274 | 275 | ```c++ 276 | void Subject::notify(const Entity& entity, Event event) 277 | { 278 | Observer* observer = head_; 279 | while (observer != NULL) 280 | { 281 | observer->onNotify(entity, event); 282 | observer = observer->next_; 283 | } 284 | } 285 | ``` 286 | 287 | 在这里,我们遍历了整条链表,通知到了每一个观察者,但是这是在所有观察者相互独立且拥有相同优先级的前提之下的,如果我们需要观察者在收到通知时告知我们来决定是否继续向下遍历链表,这就十分接近 “职责链模式” 了 288 | 289 | 注意,由于我们使用了观察者对象作为链表的节点,这就限制了它只能存在于一个观察者链表中,通俗而言,也就是说:一个观察者一次只能观察一个主题;而在通常的实现中,一个观察者可能需要观察多个不同的主题(如是首次否坠落下桥的主题可能需要同时观察物理系统和存档系统),这就是下面要记录的这种实现方式了…… 290 | 291 | + 链表节点池 292 | 293 | 没错,现在依然是链表,但是每个节点不再是对象,而是一个完全由指针组成的结构,它保存了两个指针:一个是下一个节点的 `next_` 指针,另一个是指向真正观察者对象的 `observer_` 指针,如下图所示: 294 | 295 | ![观察者模式_3.png](./img/观察者模式_3.png) 296 | 297 | > 后者的风格被成为 “侵入式” 链表,因为在对象内部使用链表侵入了对象本身的定义。侵入式链表灵活性更小,但如我们所见,也更有效率。在 Linux 核心这样的地方这种风格很流行 298 | 299 | 那么,该如何避免内存动态分配呢?很简单,由于这些节点都是同样的大小和类型,我们可以预先在 “[对象池](./对象池.md)” 中分配它们 300 | 301 | ## 善后问题 302 | 303 | 一个十分常见且可能导致严重后果的事情可能发生:如果我们不小心在某个观察者的代码中调用了 `delete`,观察者被成功地销毁了,但那是被观察者却还保留着指向它的指针,指向的是一片已经被释放的内存区域,如果被观察者试图通过发送一个通知,那么后果可能会让这个游戏崩溃 304 | 305 | Bob Nystrom 在书中抱怨说,似乎大部分的设计模式类书籍都没有说明这个问题 306 | 307 | 删除被观察者更容易些,因为在多数实现中观察者没有对他的引用;但是,直接删除被观察者有些时候也会产生问题:当观察者仍认为自己在观察某个主题时,并且希望收到来自它的消息,这时它的愿望就会落空;所以,我们需要给它的析构函数添加 `removeObserver()` 308 | 309 | > 通常在这种情况下,难点不再如何做,而在记得做 310 | 311 | 综上,在内存释放时,我们最好需要考虑清楚两点情况:一,我们需要让被观察者知晓某个观察者已经离开;二,我们需要让观察者知道自己观察的主题已经失效 312 | 313 | Bob Nystrom 在书中提及了更安全的方案,那就是使用双向指针实现自动取消注册,但这极大地增加了代码的复杂度,相信只有对指针管理熟练的程序员才能完全驾驭得了它 314 | 315 | 即使在拥有内存自动管理机制的语言中,这一部分内存释放的问题同样需要谨慎对待,多数内存回收会进行引用计数,当我们仍在代码的某处留存有对观察者或被观察者的引用时,它们并不会得到真正的释放,这就会无形之中浪费 CPU 循环和内存占用,而且在多数情况下这种错误更不容易让人察觉;在通知系统中,这种情况的专名叫 “失效监听者问题” 316 | 317 | ## 拓展 318 | 319 | 观察者在许多代码中都有极其相似的实现,它无非做了两件事: 320 | 321 | + 获知有状态改变了 322 | + 下命令改变一些数据来反映新的状态 323 | 324 | > 计算机科学学术界和软件工程师已经用了很长时间尝试结束这种状况了。这些方式被赋予了不同的名字:“数据流编程”,“函数反射编程” 等等 325 | 326 | 特例如 “数据绑定”,它不再指望完全弃用命令式代码,也不尝试基于巨大的声明式数据图标架构整个应用,它只是自动改变 UI 元素或计算某些数值来反映值的变化 327 | 328 | 和声明式系统类似,数据绑定还是太慢,作为游戏引擎核心的代码也太过复杂;经典的观察者模式虽然不如热门技术中充满着 “函数” “反射” 等等,但是它仍在简单且高效地工作着 329 | 330 | Bob Nystrom 在书中说道: 331 | 332 | > 对我而言,着通常是解决方案最重要的条件 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 游戏开发者笔记本 2 | 3 | [![](https://img.shields.io/github/stars/VoidmatrixHeathcliff/GameDeveloperNotes.svg?style=flat&labelColor=e49e61)](https://github.com/VoidmatrixHeathcliff/GameDeveloperNotes/stargazers) 4 | [![](https://img.shields.io/github/forks/VoidmatrixHeathcliff/GameDeveloperNotes.svg?style=flat&labelColor=e49e61)](https://github.com/VoidmatrixHeathcliff/GameDeveloperNotes/network/members) 5 | [![](https://img.shields.io/github/issues/VoidmatrixHeathcliff/GameDeveloperNotes.svg?style=flat&labelColor=3f48cc)](https://github.com/VoidmatrixHeathcliff/GameDeveloperNotes/issues) 6 | ![](https://img.shields.io/github/license/VoidmatrixHeathcliff/GameDeveloperNotes.svg?style=flat&label=license&message=notspecified&labelColor=3f48cc) 7 | [![](https://img.shields.io/github/contributors/VoidmatrixHeathcliff/GameDeveloperNotes)](https://github.com/VoidmatrixHeathcliff/GameDeveloperNotes/graphs/contributors) 8 | ![](https://img.shields.io/github/commit-activity/m/VoidmatrixHeathcliff/GameDeveloperNotes) 9 | ![](https://jwenjian-visitor-badge-5.glitch.me/badge?page_id=VoidmatrixHeathcliff.GameDeveloperNotes.readme) 10 | 11 | ## 这是什么? 12 | 13 | 这里是 Voidmatrix 的游戏开发笔记 14 | 15 | Voidmatrix 是一个空有一腔热血但是没有半点实际本领的人,所以 Voidmatrix 打算在理论知识的海洋中麻痹自己,以忘却自己在实践上的羸弱带来的痛苦 16 | 17 | Voidmatrix 在把几本书反反复复看了数遍之后发现自己什么也没有记住,万般羞愧之下终于记起了人类手中名为 “笔记” 的传奇发明,希望借此帮助自己愚蠢的大脑记住一星半点的知识 18 | 19 | 除此之外,希望记录在这里的知识也能帮到你 20 | 21 | ## 这里有啥? 22 | 23 | 笔记的内容可能十分零碎且覆盖面极广,从让 Voidmatrix 头疼的算法到 Voidmatrix 永远无法掌握的设计模式们,甚至是关于 Voidmatrix 只是云玩过的各类游戏的剖析和总结;笔记尽可能地覆盖 Voidmatrix 所接触过的游戏开发者的知识素养,但是,它可能并不完全适合刚刚入门游戏开发的初学者 24 | 25 | Voidmatrix 会尽自己最大的可能把笔记本打理得井井有条,但是由于很多内容正在施工,且 Voidmatrix 正在迫于学业和生计的双重打压,在某些情况下整本内容看起来可能会处于奇怪的状态 26 | 27 | ~~另外,代码会有的,目录也会有的~~ 28 | 29 | ## 关于协议? 30 | 31 | 十分奇怪,项目使用了 **MPL-2.0**,但这是 Voidmatrix 想了很久之后才决定的 32 | 33 | 这将意味着,所有在本项目基础之上搭建起来的高楼大厦,都必须秉持开放 源代码(或原始内容)的初衷,并且在自己的项目中采用相同的许可证,除此之外,你可能还要对新增的部分提供足够的说明文档 34 | 35 | 最后,还要感谢 [VisualDust](https://github.com/visualDust) 的 [ml.akasaki.space](https://github.com/visualDust/ml.akasaki.space) 项目为本 README 编写带来了足够的灵感 36 | 37 | ****** 38 | 39 | 最后,如果你喜欢这个笔记本,欢迎和我一起来填充它的内容,它或许会在未来帮助到更多像 Voidmatrix 一样热爱游戏开发但却迷失在路上的孩子们 40 | 41 | 不要忘记点亮右上角的小星星哦 ~ 42 | 43 | 欢迎联系: Voidmatrix@qq.com --------------------------------------------------------------------------------