新書推薦:
《
泰山:一种中国信仰专论(法国汉学经典译丛)
》
售價:NT$
380.0
《
花外集斠箋
》
售價:NT$
704.0
《
有兽焉.8
》
售價:NT$
305.0
《
大学问·明清经济史讲稿
》
售價:NT$
330.0
《
中国国际法年刊(2023)
》
售價:NT$
539.0
《
实用对联大全
》
售價:NT$
225.0
《
想象欧洲丛书(7册)欧洲史
》
售價:NT$
1880.0
《
没有伞的孩子必须努力奔跑
》
售價:NT$
149.0
|
編輯推薦: |
十年磨一剑,作者将设计模式理论巧妙地融入到实践中,以一个游戏的完整实现呈现设计模式的应用及经验的传承 《轩辕剑》之父蔡明宏、资深游戏制作人李佳泽、Product Evangelist at Unity TechnologiesKelvin Lo、信仁软件设计创办人 赖信仁、资深3D游戏美术刘明恺联合推荐全书采用了整合式的项目教学,即以一个游戏的范例来应用23种设计模式的实现贯穿全书,让读者学习到整个游戏开发的全过程和作者想要传承的经验,并以浅显易懂的比喻来解析难以理解的设计模式,让想深入了解此领域的读者更加容易上手。
|
內容簡介: |
《设计模式与游戏完美开发》是作者十年磨一剑,将设计模式理论巧妙地融合到实践中的最佳教材。 全书采用了整合式的项目教学,即以一个游戏的范例来应用23种设计模式的实现贯穿全书,让读者学习到整个游戏开发的全过程和作者想要传承的经验,并以浅显易懂的比喻来解析难以理解的设计模式,让想深入了解此领域的读者更加容易上手。 本书既可以作为大学、专科和职业院校游戏程序设计专业的教材,也可以作为游戏从业人员提高游戏设计能力和规范运用设计模式的培训教材,还可以作为有这方面职业兴趣的读者学习和提高的自学参考书。
|
關於作者: |
蔡升达当年为实现游戏梦从学术界进入游戏业,累积了10年游戏开发经验,包含多款大型多人线上游戏(MMORPG)、网页游戏(Web Game)、移动平台连线游戏(APP Game)。曾任职于台湾地区知名游戏开发公司,担任游戏设计师、专案程序统筹、研发技术中心经理等职务,现任知名新创团队研发总监一职。擅长游戏程序设计、网络程序设计、分布式系统设计、电脑图学、影像搜索、游戏制作规划及运作、游戏专案管理。喜欢阅读,家中有上千本藏书,包含信息技术、文学、奇幻小说、历史小说等,因为看了很多,所以也有很多想法想与人分享。构想多年之后,某日下午终于下笔开始了这本书。十年磨一剑,作者将设计模式理论巧妙地融入到实践中,以一个游戏的完整实现呈现设计模式的应用及经验的传承
|
目錄:
|
第1篇 设计模式与游戏设计
第1章 游戏实现中的设计模式 2
1.1 设计模式的起源 2
1.2 软件的设计模式是什么? 3
1.3 面向对象设计中常见的设计原则 4
1.4 为什么要学习设计模式 7
1.5 游戏程序设计与设计模式 8
1.6 模式的应用与学习方式 10
1.7 结论 11
第2章 游戏范例说明 12
2.1 游戏范例 12
2.2 GoF的设计模式范例 15
第2篇 基础系统
第3章 游戏场景的转换状态模式(State) 20
3.1 游戏场景 20
3.1.1 场景的转换 20
3.1.2 游戏场景可能的实现方式 23
3.2 状态模式(State) 24
3.2.1 状态模式(State)的定义 24
3.2.2 状态模式(State)的说明 25
3.2.3 状态模式(State)的实现范例 25
3.3 使用状态模式(State)实现游戏场景的转换 28
3.3.1 SceneState的实现 28
3.3.2 实现说明 29
3.3.3 使用状态模式(State)的优点 35
3.3.4 游戏执行流程及场景转换说明 36
3.4 状态模式(State)面对变化时 37
3.5 结论 37
第4章 游戏主要类外观模式(Facade) 39
4.1 游戏子功能的整合 39
4.2 外观模式(Facade) 41
4.2.1 外观模式(Facade)的定义 41
4.2.2 外观模式(Facade)的说明 42
4.2.3 外观模式(Facade)的实现说明 43
4.3 使用外观模式(Facade)实现游戏主程序 44
4.3.1 游戏主程序架构设计 44
4.3.2 实现说明 45
4.3.3 使用外观模式(Facade)的优点 47
4.3.4 实现外观模式(Facade)时的注意事项 48
4.4 外观模式(Facade)面对变化时 48
4.5 结论 48
第5章 获取游戏服务的唯一对象单例模式(Singleton) 50
5.1 游戏实现中的唯一对象 50
5.2 单例模式(Singleton) 51
5.2.1 单例模式(Singleton)的定义 51
5.2.2 单例模式(Singleton)的说明 51
5.2.3 单例模式(Singleton)的实现范例 52
5.3 使用单例模式(Singleton)获取唯一的游戏服务对象 53
5.3.1 游戏服务类的单例模式实现 53
5.3.2 实现说明 54
5.3.3 使用单例模式(Singleton)后的比较 55
5.3.4 反对使用单例模式(Singleton)的原因 55
5.4 少用单例模式(Singleton)时如何方便地引用到单一对象 58
5.5 结论 63
第6章 游戏内各系统的整合中介者模式(Mediator) 64
6.1 游戏系统之间的沟通 64
6.2 中介者模式(Mediator) 68
6.2.1 中介者模式(Mediator)的定义 69
6.2.2 中介者模式(Mediator)的说明 69
6.2.3 中介者模式(Mediator)的实现范例 69
6.3 中介者模式(Mediator)作为系统之间的沟通接口 72
6.3.1 使用中介者模式(Mediator)的系统架构 73
6.3.2 实现说明 73
6.3.3 使用中介者模式(Mediator)的优点 79
6.3.4 实现中介者模式(Mediator)时的注意事项 79
6.4 中介者模式(Mediator)面对变化时 80
6.5 结论 80
第7章 游戏的主循环Game Loop 82
7.1 GameLoop由此开始 82
7.2 怎么实现游戏循环(Game Loop) 84
7.3 在Unity3D中实现游戏循环 85
7.4 P级阵地的游戏循环 89
7.5 结论 92
第3篇 角色的设计
第8章 角色系统的设计分析 94
8.1 游戏角色的架构 94
8.2 角色类的规划 95
第9章 角色与武器的实现桥接模式(Bridge) 98
9.1 角色与武器的关系 98
9.2 桥接模式(Bridge) 103
9.2.1 桥接模式(Bridge)的定义 103
9.2.2 桥接模式(Bridge)的说明 107
9.2.3 桥接模式(Bridge)的实现范例 108
9.3 使用桥接模式(Bridge)实现角色与武器接口 110
9.3.1 角色与武器接口设计 110
9.3.2 实现说明 111
9.3.3 使用桥接模式(Bridge)的优点 116
9.3.4 实现桥接模式(Bridge)的注意事项 116
9.4 桥接模式(Bridge)面对变化时 116
9.5 结论 117
第10章 角色属性的计算策略模式(Strategy) 118
10.1 角色属性的计算需求 118
10.2 策略模式(Strategy) 121
10.2.1 策略模式(Strategy)的定义 122
10.2.2 策略模式(Strategy)的说明 122
10.2.3 策略模式(Strategy)的实现范例 123
10.3 使用策略模式(Strategy)实现攻击计算 124
10.3.1 攻击流程的实现 125
10.3.2 实现说明 125
10.3.3 使用策略模式(Strategy)的优点 132
10.3.4 实现策略模式(Strategy)时的注意事项 133
10.4 策略模式(Strategy)面对变化时 134
10.5 结论 135
第11章 攻击特效与击中反应模板方法模式(Template Method) 137
11.1 武器的攻击流程 137
11.2 模板方法模式(Template Method) 139
11.2.1 模板方法模式(Template Method)的定义 139
11.2.2 模板方法模式(Template Method)的说明 141
11.2.3 模板方法模式(Template Method)的实现范例 141
11.3 使用模板方法模式实现攻击与击中流程 142
11.3.1 攻击与击中流程的实现 143
11.3.2 实现说明 143
11.3.3 运用模板方法模式(Template Method)的优点 145
11.3.4 修改击中流程的实现 145
11.4 模板方法模式(Template Method)面对变化时 147
11.5 结论 149
第12章 角色AI状态模式(State) 150
12.1 角色的AI 150
12.2 状态模式(State) 158
12.3 使用状态模式(State)实现角色AI 159
12.3.1 角色AI的实现 159
12.3.2 实现说明 160
12.3.3 使用状态模式(State)的优点 169
12.3.4 角色AI执行流程 169
12.4 状态模式(State)面对变化时 170
12.5 结论 172
第13章 角色系统 174
13.1 角色类 174
13.2 游戏角色管理系统 176
第4篇 角色的产生
第14章 游戏角色的产生工厂方法模式(Factory Method) 183
14.1 产生角色 183
14.2 工厂方法模式(Factory Method) 188
14.2.1 工厂方法模式(Factory Method)的定义 188
14.2.2 工厂方法模式(Factory Method)的说明 189
14.2.3 工厂方法模式(Factory Method)的实现范例 189
14.3 使用工厂方法模式(Factory Method)产生角色对象 195
14.3.1 角色工厂类 195
14.3.2 实现说明 196
14.3.3 使用工厂方法模式(Factory Method)的优点 199
14.3.4 工厂方法模式(Factory Method)的实现说明 199
14.4 工厂方法模式(Factory Method)面对变化时 203
14.5 结论 205
第15章 角色的组装建造者模式(Builder) 206
15.1 角色功能的组装 206
15.2 建造者模式(Builder) 213
15.2.1 建造者模式(Builder)的定义 213
15.2.2 建造者模式(Builder)的说明 214
15.2.3 建造者模式(Builder)的实现范例 215
15.3 使用建造者模式(Builder)组装角色的各项功能 217
15.3.1 角色功能的组装 218
15.3.2 实现说明 219
15.3.3 使用建造者模式(Builder)的优点 226
15.3.4 角色建造者的执行流程 226
15.4 建造者模式(Builder)面对变化时 227
15.5 结论 228
第16章 游戏属性管理功能享元模式(Flyweight) 229
16.1 游戏属性的管理 229
16.2 享元模式(Flyweight) 236
16.2.1 享元模式(Flyweight)的定义 236
16.2.2 享元模式(Flyweight)的说明 237
16.2.3 享元模式(Flyweight)的实现范例 238
16.3 使用享元模式(Flyweight)实现游戏 242
16.3.1 SceneState的实现 242
16.3.2 实现说明 245
16.3.3 使用享元模式(Flyweight)的优点 250
16.3.4 享元模式(Flyweight)的实现说明 250
16.4 享元模式(Flyweight)面对变化时 252
16.5 结论 252
第5篇 战争开始
第17章 Unity3D的界面设计组合模式(Composite) 254
17.1 玩家界面设计 254
17.2 组合模式(Composite) 259
17.2.1 组合模式(Composite)的定义 259
17.2.2 组合模式(Composite)的说明 260
17.2.3 组合模式(Composite)的实现范例 261
17.2.4 分了两个子类但是要使用同一个操作界面 264
17.3 Unity3D游戏对象的分层式管理功能 265
17.3.1 游戏对象的分层管理 265
17.3.2 正确有效地获取UI的游戏对象 266
17.3.3 游戏用户界面的实现 267
17.3.4 兵营界面的实现 269
17.4 结论 274
第18章 兵营系统及兵营信息显示 276
18.1 兵营系统 276
18.2 兵营系统的组成 277
18.3 初始兵营系统 281
18.4 兵营信息的显示流程 287
第19章 兵营训练单位命令模式(Command) 288
19.1 兵营界面上的命令 288
19.2 命令模式(Command) 291
19.2.1 命令模式(Command)的定义 291
19.2.2 命令模式(Command)的说明 294
19.2.3 命令模式(Command)的实现范例 294
19.3 使用命令模式(Command)实现兵营训练角色 297
19.3.1 训练命令的实现 297
19.3.2 实现说明 298
19.3.3 执行流程 302
19.3.4 实现命令模式(Command)时的注意事项 303
19.4 命令模式(Command)面对变化时 305
19.5 结论 306
第20章 关卡设计责任链模式(Chain of Responsibility) 307
20.1 关卡设计 307
20.2 责任链模式(Chain of Responsibility) 312
20.2.1 责任链模式(Chain of Responsibility)的定义 312
20.2.2 责任链模式(Chain of Responsibility)的说明 314
20.2.3 责任链模式(Chain of Responsibility)的实现范例 314
20.3 使用责任链模式(Chain of Responsibility)实现关卡系统 317
20.3.1 关卡系统的设计 317
20.3.2 实现说明 318
20.3.3 使用责任链模式(Chain of Responsibility)的优点 329
20.3.4 实现责任链模式(Chain of Responsibility)时的注意事项 329
20.4 责任链模式(Chain of Responsibility)面对变化时 330
20.5 结论 332
第6篇 辅助系统
第21章 成就系统观察者模式(Observer) 334
21.1 成就系统 334
21.2 观察者模式(Observer) 338
21.2.1 观察者模式(Observer)的定义 338
21.2.2 观察者模式(Observer)的说明 340
21.2.3 观察者模式(Observer)的实现范例 341
21.3 使用观察者模式(Observer)实现成就系统 344
21.3.1 成就系统的新架构 344
21.3.2 实现说明 346
21.3.3 使用观察者模式(Observer)的优点 358
21.3.4 实现观察者模式(Observer)时的注意事项 358
21.4 观察者模式(Observer)面对变化时 359
21.5 结论 361
第22章 存盘功能备忘录模式(Memento) 362
22.1 存储成就记录 362
22.2 备忘录模式(Memento) 366
22.2.1 备忘录模式(Memento)的定义 366
22.2.2 备忘录模式(Memento)的说明 367
22.2.3 备忘录模式(Memento)的实现范例 367
22.3 使用备忘录模式(Memento)实现成就记录的保存 371
22.3.1 成就记录保存的功能设计 371
22.3.2 实现说明 371
22.3.3 使用备忘录模式(Memento)的优点 374
22.3.4 实现备忘录模式(Memento)的注意事项 374
22.4 备忘录模式(Memento)面对变化时 374
22.5 结论 375
第23章 角色信息查询访问者模式(Visitor) 376
23.1 角色信息的提供 376
23.2 访问者模式(Visitor) 385
23.2.1 访问者模式(Visitor)的定义 386
23.2.2 访问者模式(Visitor)的说明 390
23.2.3 访问者模式(Visitor)的实现范例 392
23.3 使用访问者模式(Visitor)实现角色信息查询 397
23.3.1 角色信息查询的实现设计 397
23.3.2 实现说明 398
23.3.3 使用访问者模式(Visitor)的优点 405
23.3.4 实现访问者模式(Visitor)时的注意事项 405
23.4 访问者模式(Visitor)面对变化时 405
23.5 结论 408
第7篇 调整与优化
第24章 前缀字尾装饰模式(Decorator) 410
24.1 前缀后缀系统 410
24.2 装饰模式(Decorator) 415
24.2.1 装饰模式(Decorator)的定义 415
24.2.2 装饰模式(Decorator)的说明 418
24.2.3 装饰模式(Decorator)的实现范例 419
24.3 使用装饰模式(Decorator)实现前缀后缀的功能 422
24.3.1 前缀后缀功能的架构设计 423
24.3.2 实现说明 423
24.3.3 使用装饰模式(Decorator)的优点 433
24.3.4 实现装饰模式(Decorator)时的注意事项 433
24.4 装饰模式(Decorator)面对变化时 434
24.5 结论 435
第25章 俘兵适配器模式(Adapter) 436
25.1 游戏的宠物系统 436
25.2 适配器模式(Adapter) 440
25.2.1 适配器模式(Adapter)的定义 440
25.2.2 适配器模式(Adapter)的说明 441
25.2.3 适配器模式(Adapter)的实现范例 441
25.3 使用适配器模式(Adapter)实现俘兵系统 443
25.3.1 俘兵系统的架构设计 443
25.3.2 实现说明 443
25.3.3 与俘兵相关的新增部分 445
25.3.4 使用适配器模式(Adapter)的优点 450
25.4 适配器模式(Adapter)面对变化时 450
25.5 结论 451
第26章 加载速度的优化代理模式(Proxy) 453
26.1 最后的系统优化 453
26.2 代理模式(Proxy) 457
26.2.1 代理模式(Proxy)的定义 458
26.2.2 代理模式(Proxy)的说明 458
26.2.3 代理模式(Proxy)的实现范例 459
26.3 使用代理模式(Proxy)测试和优化加载速度 460
26.3.1 优化加载速度的架构设计 460
26.3.2 实现说明 461
26.3.3 使用代理模式(Proxy)的优点 464
26.3.4 实现代理模式(Proxy)时的注意事项 464
26.4 代理模式(Prory)面对变化时 466
26.5 结论 466
第8篇 未明确使用的模式
第27章 迭代器模式(Iterator)、原型模式(Prototype)和解释器模式(Interpreter) 468
27.1 迭代器模式(Iterator) 468
27.2 原型模式(Prototype) 469
27.3 解释器模式(Interpreter) 471
第28章 抽象工厂模式(Abstract Factory) 472
28.1 抽象工厂模式(Abstract Factory)的定义 472
28.2 抽象工厂模式(Abstract Factory)的实现 473
28.3 可应用抽象工厂模式的场合 476
参考文献 477
|
內容試閱:
|
初次接触设计模式(Design Patterns)是在求学阶段,第一次看GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》时,感觉尤如天书一般,只能大概了解Singleton、Strategy、Facade、Iterator这几个Pattern的用法,至于为什么要使用、什么时候使用,完全没有概念。进入职场后,先是跟着几个大型游戏项目一同开发和学习,到后来,自己可以主持技术项目、开发网络游戏引擎、游戏框架等。在这个过程中,时而拿起GoF的《Design Patterns》或是以设计模式为题的书籍,反复阅读,逐渐地了解了每一种模式的应用以及它们的设计分析原理,并通过不断地实践与应用,才将它们融入自己的知识体系中。从2004年进入职场,一晃眼,在游戏业也超过了10的经历,这些年在游戏行业工作的付出,除了得以温饱之外,也从中吸收了不少的知识与经验。记得某天在一个项目开发会议中,我与同仁分享如何将设计模式应用在游戏的开发设计中时,我突然察觉,应该将这些内容写下来,并分享给更多的游戏设计师,于是就有了写这本书的想法。通过写作将经验与大家分享,希望大家可以了解,在游戏行业中的工程师,不该只是进行着无意义程序代码输出的码农,而是一群从事高级软件分析实现的设计师。所以,整合多种领域知识于一身的游戏工程师,更需要以优雅的方式来呈现这些知识汇集的结果,设计模式(Design Patterns)是各种软件设计技巧的呈现方式,善用它们,更能表现出游戏设计工程师优雅的一面。10年的游戏从业过程,接受过许多人的协助及帮忙:Jimmy & Silent兄弟20年的同学、朋友及合作伙伴们,有你们一路的协助与砥砺才能有今天;Justin Lee谢谢你的信任,也感谢你的忍受功力,可以让我们一同完成不少作品;Mark Tsai谢谢你一路的提拔与信任;Jazzdog感谢你的支持,我一直知道程序与美术是可以同时存在于一个人身上的;Kai合作伙伴,感谢你的支持。最后谢谢我的家人,感谢老婆大人这10多年来忍受我在书房内不断地堆积书本、小说及收藏品。感谢我3岁的女儿,因为你的到来,让我知道没什么比你更重要了。
蔡升达2016年10月
第3章游戏场景的转换状态模式(State)
3.1 游戏场景本书使用Unity3D游戏引擎作为开发工具,而Unity3D是使用场景(Scene)作为游戏运行时的环境。开始制作游戏时,开发者会将游戏需要的素材(3D模型、游戏对象)放到一个场景中,然后编写对应的程序代码,之后只要单击Play按钮,就可以开始运行游戏。除了Unity3D之外,笔者过去开发游戏时使用的游戏引擎(Game Engine)或开发框架(SDK、Framework),多数也都存在场景的概念,例如:? 早期Java Phone的J2ME开发SDK中使用的Canvas类;? Android的Java开发SDK中使用的Activity类;? iOS上2D游戏开发工具Cocos2D中使用的CCScene类。虽然各种工具不见得都使用场景(Scene)这个名词,但在实现上,一样可使用相同的方式来呈现。而上面所列的各个类,都可以被拿来作为游戏实现中场景转换的目标。3.1.1 场景的转换当游戏比较复杂时,通常会设计成多个场景,让玩家在几个场景之间转换,某一个场景可能是角色在一个大地图上行走,而另一个场景则是在地下洞穴探险。这样的设计方式其实很像是舞台剧的呈现方式,编剧们设计了一幕幕的场景让演员们在其间穿梭演出,而每幕之间的差异,可能是在布景摆设或参与演出角色的不同,但对于观众来说,同时间只会看到演员们在某一个场景中的演出。要怎样才能真正应用场景来开发游戏呢?读者可以回想一下,当我们打开游戏App或开始运行游戏软件时,会遇到什么样的画面:出现游戏Logo、播放游戏片头、加载游戏数据、出现游戏主画面、等待玩家登录游戏、进入游戏主画面,接下来玩家可能是在大地图上打怪或进入副本刷关卡。游戏画面转换如图3-1所示。图3-1 游戏画面转换就以上面的说明为例,我们可规划出下面数个场景,每个场景分别负责多项功能的执行,如图3-2所示。图3-2 每个场景负责执行的游戏功能? 登录场景:负责游戏片头、加载游戏数据、出现游戏主画面、等待玩家登录游戏。? 主画面场景:负责进入游戏画面、玩家在主城主画面中的操作、在地图上打怪打宝? 战斗场景:负责与玩家组队之后进入副本关卡、挑战王怪在游戏场景规划完成后,就可以利用状态图将各场景的关系连接起来,并且说明它们之间的转换条件以及状态转换的流程,如图3-3所示。图3-3 各场景转换条件以及状态转换的状态图即便我们换了一个游戏类型来实现,一样可以使用相同的场景分类方式,将游戏功能进行归类,例如:卡牌游戏可按如下分类:? 登录场景:负责游戏片头、加载游戏数据、出现游戏主画面、等待玩家登录游戏。? 主画面场景:玩家抽卡片、合成卡牌、查看卡牌? 战斗场景:挑战关卡、卡片对战转珠游戏可按如下分类:? 登录场景:负责游戏片头、加载游戏数据、出现游戏主画面、等待玩家登录游戏。? 主画面场景:查看关卡进度、关卡信息、商城、抽转珠? 战斗场景:挑战关卡、转珠对战当然,如果是更复杂的单机版游戏或大型多人在线游戏(MMORPG),还可以再细分出多个场景来负责对应的游戏功能。切分场景的好处将游戏中不同的功能分类在不同的场景中来执行,除了可以将游戏功能执行时需要的环境明确分类之外,重复使用也是使用场景转换的好处之一。从上面几个例子中可以看出,登录场景几乎是每款游戏必备的场景之一。而一般在登录场景中,会实现游戏初始化功能或玩家登录游戏时需要执行的功能,例如:? 单机游戏:登录场景可以有加载游戏数据、让玩家选择存盘、进入游戏等步骤。? 在线游戏:登录场景包含了许多复杂的在线登录流程,比如使用第三方认证系统、使用玩家自定义账号、与服务器连接、数据验证对于大多数的游戏开发公司来说,登录场景实现的功能,会希望通用于不同的游戏开发项目,使其保持流程的一致性。尤其对于在线游戏这种类型的项目而言,由于登录流程较为复杂,若能将各项目共同的部分(场景)独立出来,由专人负责开发维护并同步更新给各个项目,那么效率就能获得提升,也是比较安全的方式。在项目开发时,若是能重复使用这些已经设计良好的场景,将会减少许多开发时间。更多的优点将在后续章节中说明。本书范例场景的规划在本书范例中,《P级阵地》规划了3个场景,如图3-4所示。图3-4 《P级阵地》规划的3个场景? 开始场景(StarScene):GameLoop游戏对象(GameObject)的所在,游戏启动及相关游戏设置的加载。? 主画面场景(MainMenuScene):显示游戏名称和开始按钮。? 战斗场景(BattleScene):游戏主要执行的场景。3.1.2 游戏场景可能的实现方式实现Unity3D的场景转换较为直接的方式如下:Listing 3-1 一般场景控制的写法public class SceneManager{private string m_state = "开始"; 改换场景public void ChangeScenestring StateName {m_state = StateName;
switchm_state{case "菜单":Application.LoadLevelMainMenuScene;break;case "主场景":Application.LoadLevelGameScene;break;}} 更新public void Update {switchm_state{case "开始":...break;case "菜单":...break;case "主场景":...break;}}}
上述的实现方式会有以下缺点:只要增加一个状态,则所有switchm_state的程序代码都需要增加对应的程序代码。与每一个状态有关的对象,都必须在SceneManager类中被保留,当这些对象被多个状态共享时,可能会产生混淆,不太容易识别是由哪个状态设置的,造成游戏程序调试上的困难。每一个状态可能使用不同的类对象,容易造成SceneManager类过度依赖其他类,让SceneManager类不容易移植到其他项目中。为了避免出现上述缺点,修正的目标会希望使用一个场景类来负责维护一个场景,让与此场景相关的程序代码和对象能整合在一起。这个负责维护的场景类,其主要工作如下:? 场景初始化;? 场景结束后,负责清除资源;? 定时更新游戏逻辑单元;? 转换到其他场景;? 其他与该场景有关的游戏实现。由于在范例程序中我们规划了3个场景,所以会产生对应的3个场景类,但如何让这3个场景类相互合作、彼此转换呢?我们可以使用GoF的状态模式(State)来解决这些问题。3.2 状态模式(State)状态模式(State),在多数的设计模式书籍中都会提及,它也是游戏程序设计中应用最频繁的一种模式。主要是因为状态经常被应用在游戏设计的许多环节中,包含AI人工智能状态、账号登录状态、角色状态3.2.1 状态模式(State)的定义状态模式(State),在GoF中的解释是:让一个对象的行为随着内部状态的改变而变化,而该对象也像是换了类一样。如果将GoF对状态模式(State)的定义改以游戏的方式来解释,就会像下面这样:当德鲁伊(对象)由人形变化为兽形状态(内部状态改变)时,他所施展的技能(对象的行为)也会有所变化,玩家此时就像是在操作另一个不同的角色(像是换了类)。德鲁伊是一种经常出现在角色扮演游戏(RPG)中的角色名称。变化外形是他们常使用的能力,通过外形的变化,使德鲁伊具备了转换为其他形体的能力,而变化为兽形是比较常见的游戏设计。当玩家决定施展外形转换能力时,德鲁伊会进入兽形状态,这时候的德鲁伊会以兽形来表现其行为,包含移动和攻击施展的方式;当玩家决定转换回人形时,德鲁伊会复原为一般形态,继续与游戏世界互动。所以,变化外形的能力可以看成是德鲁伊的一种内部状态的转换。通过变化外形的结果,角色表现出另外一种行为模式,而这一切的转化过程都可以由德鲁伊的内部控制功能来完成,玩家不必理解这个转化过程。但无论怎么变化,玩家操作的角色都是德鲁伊,并不会因为他内部状态的转变而有所差异。当某个对象状态改变时,虽然它表现的行为会有所变化,但是对于客户端来说,并不会因为这样的变化,而改变对它的操作方法或信息沟通的方式。也就是说,这个对象与外界的对应方式不会有任何改变。但是,对象的内部确实是会通过更换状态类对象的方式来进行状态的转换。当状态对象更换到另一个类时,对象就会通过新的状态类,表现出它在这个状态下该有的行为。但这一切只会发生在对象内部,对客户端来说,完全不需要了解这些状态转换的过程及对应的方式。3.2.2 状态模式(State)的说明状态模式(State)的结构如图3-5所示。图3-5 状态模式的结构图参与者的说明如下:? Context(状态拥有者)? 是一个具有状态属性的类,可以制定相关的接口,让外界能够得知状态的改变或通过操作让状态改变。? 有状态属性的类,例如:游戏角色有潜行、攻击、施法等状态;好友上线、脱机、忙碌等状态;GoF使用TCP联网为例,有已连接、等待连接、断线等状态。这些类中会有一个ConcreteState[X]子类的对象为其成员,用来代表当前的状态。? State(状态接口类):制定状态的接口,负责规范Context(状态拥有者)在特定状态下要表现的行为。? ConcreteState(具体状态类)? 继承自State(状态接口类)。? 实现Context(状态拥有者)在特定状态下该有的行为。例如,实现角色在潜行状态时该有的行动变缓、3D模型变半透明、不能被敌方角色察觉等行为。3.2.3 状态模式(State)的实现范例首先定义Context类:Listing 3-2 定义Context类State.cspublic class Context{State m_State = null;
public void Requestint Value {m_State.HandleValue;}
public void SetStateState theState {Debug.Log "Context.SetState:" theState;m_State = theState;}}Context类中,拥有一个State属性用来代表当前的状态,外界可以通过Request方法,让Context类呈现当前状态下的行为。SetState方法可以指定Context类当前的状态,而State状态接口类则用来定义每一个状态该有的行为:Listing 3-3 State类State.cspublic abstract class State{protected Context m_Context = null;public StateContext theContext {m_Context = theContext;} public abstract void Handleint Value;}在产生State类对象时,可以传入Context类对象,并将其指定给State的类成员m_Context,让State类在后续的操作中,可以获取Context对象的信息或操作Context对象。然后定义Handle抽象方法,让继承的子类可以重新定义该方法,来呈现各自不同的状态行为。最后定义3个继承自State类的子类:Listing 3-4 定义3个状态State.cs 状态Apublic class ConcreteStateA : State{public ConcreteStateAContext theContext:basetheContext{}
public override void Handle int Value {Debug.Log "ConcreteStateA.Handle";if Value 10m_Context.SetState new ConcreteStateBm_Context;}}
状态Bpublic class ConcreteStateB : State{public ConcreteStateBContext theContext:basetheContext{}
public override void Handle int Value {Debug.Log "ConcreteStateB.Handle";if Value 20m_Context.SetState new ConcreteStateCm_Context;} }
状态Cpublic class ConcreteStateC : State{public ConcreteStateCContext theContext:basetheContext{}public override void Handle int Value {Debug.Log "ConcreteStateC.Handle";if Value 30m_Context.SetState new ConcreteStateAm_Context;} }上述3个子类,都要重新定义父类State的Handle抽象方法,用来表示在各自状态下的行为。在范例中,我们先让它们各自显示不同的信息(代表当前的状态行为),再按照本身状态的行为定义来判断是否要通知Context对象转换到另一个状态。Context类中提供了一个SetState方法,让外界能够设置Context对象当前的状态,而所谓的外界,也可以是由另一个State状态来调用。所以实现上,状态的转换可以有下列两种方式:? 交由Context类本身,按条件在各状态之间转换;? 产生Context类对象时,马上指定初始状态给Context对象,而在后续执行过程中的状态转换则交由State对象负责,Context对象不再介入。笔者在实现时,大部分情况下会选择第2种方式,原因在于:状态对象本身比较清楚在什么条件下,可以让Context对象转移到另一个State状态。所以在每个ConcreteState类的程序代码中,可以看到状态转换条件的判断,以及设置哪一个ConcreteState对象成为新的状态。每个ConcreteState状态都可以保持自己的属性值,作为状态转换或展现状态行为的依据,不会与其他的ConcreteState状态混用,在维护时比较容易理解。因为判断条件及状态属性都被转换到ConcreteState类中,故而可缩减Context类的大小。4个类定义好之后,我们可以通过测试范例来看看客户端程序会怎样利用这个设计:Listing 3-5 State的测试范例StateTest.csvoid UnitTest {Context theContext = new Context;theContext.SetState new ConcreteStatAtheContext ;
theContext.Request 5 ;theContext.Request 15 ;theContext.Request 25 ;theContext.Request 35 ;}首先产生Context对象theContext,并立即设置为ConcreteStateA状态;然后调用Context类的Request方法,并传入作为状态转换判断用途的参数,让当前状态ConcreteStateA判断是否要转移到ConcreteStateB;最后调用几次Request方法,并传入不同的参数。从输出的信息中可以看到,Context对象的状态由ConcreteStateA按序转换到ConcreteStateB、 ConcreteStateC状态,最后回到ConcreteStateA状态。 State测试范例产生的信息Context.SetState:DesignPattern_State.ConcreteStateAConcreteStateA.HandleConcreteStateA.HandleContext.SetState:DesignPattern_State.ConcreteStateBContext.SetState:DesignPattern_State.ConcreteStateCContext.SetState:DesignPattern_State.ConcreteStateA3.3 使用状态模式(State)实现游戏场景的转换在Unity3D的环境中,游戏只会在一个场景中运行,所以我们可以让每个场景都由一个场景类来负责维护。此时,如果将场景类当成状态来比喻的话,那么就可以利用状态模式(State)的转换原理,来完成场景转换的功能。由于每个场景所负责执行的功能不同,通过状态模式(State)的状态转移,除了可以实现游戏内部功能的转换外,对于客户端来说,也不必根据不同的游戏状态来编写不同的程代码,同时也减少了外界对于不同游戏状态的依赖性。而原本的Unity3D场景转换判断功能,可以在各自的场景类中完成,并且状态模式(State)同时间也只会让一个状态存在(同时间只会有一个状态在运行),因此可以满足Unity3D执行时只能有一个场景(状态)存在的要求。3.3.1 SceneState的实现《P级阵地》的场景分成3个:开始场景(StarScene)、主画面场景(MainMenu Scene)和战斗场景(BattleScene),所以声明3个场景类负责对应这3个场景。这3个场景类都继承自ISceneState,而SceneStateController则是作为这些状态的拥有者(Context),最后将SceneStateController对象放入GameLoop类下,作为与Unity3D运行的互动接口,上述结构如图3-6所示。图3-6 3个场景类的结构图其中的参与者如下说明:? ISceneState:场景类的接口,定义《P级阵地》中场景转换和执行时需要调用的方法。? StartState、MainMenuState、BattleState:分别对应范例中的开始场景(StarScene)、主画面场景(MainMenuScene)及战斗场景(BattleScene),作为这些场景执行时的操作类。? SceneStateController:场景状态的拥有者(Context),保持当前游戏场景状态,并作为与GameLoop类互动的接口。除此之外,也是执行Unity3D场景转换的地方。? GameLoop:游戏主循环类作为Unity3D与《P级阵地》的互动接口,包含了初始化游戏和定期调用更新操作。3.3.2 实现说明首先,定义ISceneState接口如下:Listing 3-6 定义ISceneState类ISceneState.cspublic class ISceneState{ 状态名称private string m_StateName = "ISceneState";public string StateName {get{ return m_StateName; }set{ m_StateName = value; }}
控制者protected SceneStateController m_Controller = null; 建造者public ISceneStateSceneStateController Controller {m_Controller = Controller;}
开始public virtual void StateBegin{}
结束public virtual void StateEnd{}
更新public virtual void StateUpdate{}
public override string ToString {return string.Format "[I_SceneState: StateName={0}]", StateName;}}ISceneState定义了在《P级阵地》中,场景转换执行时需要被Unity3D通知的操作,包含:? StateBegin方法:场景转换成功后会利用这个方法通知类对象。其中可以实现在该场景执行时需要加载的资源及游戏参数的设置。SceneState Controller在此时才传入(不像前一节范例那样在建造者中传入),因为Unity3D在转换场景时会花费一些时间,所以必须先等到场景完全加载成功后才能继续执行。? StateEnd方法:场景将要被释放时会利用这个方法通知类对象。其中可以释放游戏不再使用的资源,或者重新设置游戏场景状态。? StateUpdate方法:游戏定时更新时会利用这个方法通知类对象。该方法可以让Unity3D的定时更新功能被调用,并通过这个方法让其他游戏系统也定期更新。这个方法可以让游戏系统类不必继承Unity3D的MonoBehaviour类,也可以拥有定时更新功能,第7章会对此进行更详细地说明。? m_StateName属性:可以在调试(Debug)时使用。StateBegin、StateEnd及StateUpdate这3个方法,虽然是定义为ISceneState中的接口方法,但是由于不强迫子类重新实现它,所以并没有被定义为抽象方法。共有3个子类继承自ISceneState,分别用来负责各Unity3D Scene的运行和转换。首先是负责开始场景(StarScene)的类,程序代码如下:Listing 3-7 定义开始状态类StartState.cspublic class StartState : ISceneState{public StartStateSceneStateController Controller:baseController {this.StateName = "StartState";}
开始public override void StateBegin { 可在此进行游戏数据加载和初始化等}
更新public override void StateUpdate { 更换为m_Controller.SetStatenew MainMenuStatem_Controller, "MainMenuScene";}}《P级阵地》的运行,必须在开始场景(StarScene)中单击开始按钮才能运行,所以游戏最开始的场景状态会被设置为StartState。因此在实现上,可在StateBeing方法中,将游戏启动时所需要的资源加载,这些资源可以是游戏属性数据、角色组件预载、游戏系统初始化、版本信息等。当StartState的StateUpdate第一次被调用时,会马上将游戏场景状态转换为MainMenuState,完成StartStateStartScene初始化游戏的任务。主画面场景(MainMenuScene)负责显示游戏的开始画面,并且提供简单的界面让玩家可以开始进入游戏,程序代码如下:Listing 3-8 定义主菜单状态MainMenuState.cspublic class MainMenuState : ISceneState{public MainMenuStateSceneStateController Controller:baseController {this.StateName = "MainMenuState";}
开始public override void StateBegin { 获取开始按钮Button tmpBtn = UITool.GetUIComponent"StartGameBtn";iftmpBtn!=nulltmpBtn.onClick.AddListener =OnStartGameBtnClicktmpBtn ;} 开始游戏private void OnStartGameBtnClickButton theButton {Debug.Log "OnStartBtnClick:" theButton.gameObject.name;m_Controller.SetStatenew BattleStatem_Controller, "BattleScene";}}《P级阵地》的开始画面上只有一个开始按钮,这个按钮是使用Unity3D的UI工具增加的。从原本Unity3D的UI设置界面上,可直接设置当按钮被鼠标单击时,需要由哪一个脚本组件(Script Compoment)的方法来执行;这个设置动作也可以改由程序代码来指定。至于《P级阵地》与Unity3D的UI设计工具的整合,在第17章中有进一步的说明。因此,在MainMenuState的StateBegin方法中,获取MainMenuScene的开始按钮(StartGameBtn)后,将其onClick事件的监听者设置为OnStartGameBtnClick方法,而该方法也将直接实现在MainMenuState类中。所以,当玩家单击开始按钮时,OnStartGameBtnClick会被调用,并将游戏场景状态通过SceneStateController转换到战斗场景(BattleScene)。战斗场景(BattleScene)为《P级阵地》真正游戏玩法(阵地防守)运行的场景,程序代码如下:Listing 3-9 定义战斗状态(BattleState.cs)public class BattleState : ISceneState{public BattleStateSceneStateController Controller:baseController {this.StateName = "BattleState";}
开始public override void StateBegin {PBaseDefenseGame.Instance.Initinal;}
结束public override void StateEnd {PBaseDefenseGame.Instance.Release;} 更新public override void StateUpdate { 输入InputProcess;
游戏逻辑PBaseDefenseGame.Instance.Update;
Render由Unity负责
游戏是否结束if PBaseDefenseGame.Instance.ThisGameIsOverm_Controller.SetStatenew MainMenuStatem_Controller, "MainMenuScene";}
输入private void InputProcess { 玩家输入判断程序代码}}负责战斗场景(BattleScene)的BattleState状态类在StateBegin方法中,首先调用了游戏主程序PBaseDefenseGame的初始化方法:public override void StateBegin {PBaseDefenseGame.Instance.Initinal;}当《P级阵地》在一场战斗结束或放弃战斗时,玩家可以回到主菜单场景(MainMenuState)。所以,当战斗场景(BattleScene)即将结束时,StateEnd方法就会被调用,实现上,会在此调用释放游戏主程序的操作:public override void StateEnd {PBaseDefenseGame.Instance.Release;}BattleState的StateUpdate方法扮演着游戏循环的角色(GameLoop将在第7章中说明)。先获取玩家的输入操作后,再执行游戏逻辑(调用PBase DefenseGame的Update方法),并且不断地定时重复调用,直到游戏结束转换为主菜单场景(MainMenuState)为止:public override void StateUpdate { 输入InputProcess;
游戏逻辑PBaseDefenseGame.Instance.Update;
Render由Unity负责
游戏是否结束if PBaseDefenseGame.Instance.ThisGameIsOverm_Controller.SetStatenew MainMenuStatem_Controller, "MainMenuScene";}3个主要的游戏状态类都定义完成后,接下来就是实现这些场景转换和控制的功能:Listing 3-10 定义场景状态控制者(SceneStateController.cs)public class SceneStateController{private ISceneState m_State;private bool m_bRunBegin = false;public SceneStateController{}
设置状态public void SetStateISceneState State, string LoadSceneName {Debug.Log "SetState:" State.ToString;m_bRunBegin = false;
载入场景LoadScene LoadSceneName ;
通知前一个State结束if m_State != null m_State.StateEnd;
设置m_State=State;}
载入场景private void LoadScenestring LoadSceneName {if LoadSceneName==null || LoadSceneName.Length == 0 return ;Application.LoadLevel LoadSceneName ;}
更新public void StateUpdate { 是否还在加载if Application.isLoadingLevelreturn ;
通知新的State开始if m_State != null && m_bRunBegin==false{m_State.StateBegin;m_bRunBegin = true;}
if m_State != nullm_State.StateUpdate;}}SceneStateController类中有一个ISceneState成员,用来代表当前的游戏场景状态。在SetState方法中,实现了转换场景状态的功能,该方法先使用Application.LoadLevel方法来加载场景;然后通知前一个状态的StateEnd方法来释放前一个状态;最后将传入的参数设置为当前状态。至于SceneUpdate方法,则是会先判断场景是否载入成功,成功之后才会调用当前游戏场景状态的StateBeing方法来初始化游戏场景状态。最后,将SceneStateController与GameLoop脚本组件结合如下:Listing 3-11 与游戏主循环的结合GameLoop.cspublic class GameLoop : MonoBehaviour{ 场景状态SceneStateController m_SceneStateController =new SceneStateController;
void Awake { 转换场景不会被删除GameObject.DontDestroyOnLoad this.gameObject ;
随机数种子UnityEngine.Random.seed =intDateTime.Now.Ticks;}
Use this for initializationvoid Start { 设置起始的场景m_SceneStateController.SetStatenew StartStatem_SceneStateController, "";}
Update is called once per framevoid Update {m_SceneStateController.StateUpdate;}}在GameLoop脚本组件中,定义并初始化SceneStateController类对象,并在Start方法中设置第一个游戏场景状态: StartState。之后在GameLoop脚本组件每次的Update更新方法中,调用SceneStateController对象的StateUpdate方法,让当前的场景状态类能够被定时更新。3.3.3 使用状态模式(State)的优点使用状态模式(State)来实现游戏场景转换,有下列优点:减少错误的发生并降低维护难度不再使用switchm_state来判断当前的状态,这样可以减少新增游戏状态时,因未能检查到所有switchm_state程序代码而造成的错误。状态执行环境单一化与每一个状态有关的对象及操作都被实现在一个场景状态类下,对程师设计师来说,这样可以清楚地了解每一个状态执行时所需要的对象及配合的类。项目之间可以共享场景本章开始时就提到,有些场景可以在不同项目之间共享。以当前《P级阵地》使用的3个场景及状态类为例,其中的开始场景(StartScene)和开始状态类(Start State)都可以在不同项目之间共享。例如:可以在开始状态类(StartState)的StateBegin方法中,明确定义出游戏初始化时的步骤,并将这些步骤搭配模版方法模式(Template Method)或策略模式(Strategy),就能让各项目自行定义符合各个游戏需求的具体实现,达到各项目共享场景的目的。这种做法对于网络在线型的游戏项目特别有用,在此类型的项目中,玩家的上线、登录、验证、数据同步等过程,实现上存在一定的复杂度。若将这些复杂的操作放在共享的场景中,共享使用与维护,就可以节省许多的开发时间及成本。3.3.4 游戏执行流程及场景转换说明从Unity游戏开始执行的流程来看,《P级阵地》通过StartScene场景中唯一的GameLoop游戏对象(GameObject),以及挂在其上的GameLoop脚本组件(Script Component),将整个游戏运行起来。所以,在GameLoop的Start方法中设置好第一个游戏场景状态后,GameLoop的Update方法就将游戏的控制权交给SceneStateController。而SceneStateController内部则会记录当前的游戏场景状态类,之后再通过调用游戏场景状态的StateUpdate方法,就能够完成更新当前游戏场景状态的需求。上述流程可以参考下面的流程图,如图3-7所示。图3-7 流程图3.4状态模式(State)面对变化时随着项目开发进度进入中后期,游戏企划可能会提出新的系统功能来增加游戏内容。这些提案可能是增加小游戏关卡、提供查看角色信息图鉴、玩家排行等功能。当程序人员在分析这些新增的系统需求后,如果觉得无法在现有的场景(Scene)下实现,就必须使用新的场景来完成。而在现有的架构下,程序人员只需要完成下列几项工作:? 在Unity3D编辑模式下新增场景。? 加入一个新的场景状态类对应到新的场景,并在其中实现相关功能。? 决定要从哪个现有场景转换到新的场景。? 决定新的场景结束后要转换到哪一个场景。上述流程,就程序代码的修改而言,只会新增一个程序文件(.cs)用来实现新的场景状态类,并修改一个现有的游戏状态,让游戏能按照需求转换到新的场景状态。除此之外,不需要修改其他任何的程序代码。3.5结论在本章中,我们利用状态模式(State)实现了游戏场景的切换,这种做法并非全然都是优点,但与传统的switchstate_code相比,已经算是更好的设计。此外,正如前面章节所介绍的,设计模式并非只能单独使用,在实际开发中,若多种设计模式搭配得宜,将会是更好的设计。因此,本章结尾,我们将讨论,本章所做的设计还有哪些应该注意的地方,以及还可以将状态模式(State)应用在游戏设计的哪些地方。状态模式(State)的优缺点使用状态模式(State)可以清楚地了解某个场景状态执行时所需要配合使用的类对象,并且减少因新增状态而需要大量修改现有程序代码的维护成本。《P级阵地》只规划了3个场景来完成整个游戏,算是产出较少状态类的应用。但如果状态模式(State)是应用在有大量状态的系统时,就会遇到产生过多状态类的情况,此时会伴随着类爆炸的问题,这算是一个缺点。不过与传统使用switchstate_code的实现方式相比,使用状态模式(State)对于项目后续的长期维护效益上,仍然具有优势。在本书后面(第12章)讲解到AI实现时,还会再次使用状态模式(State)来实现,届时,读者可看到其他利用状态模式(State)的应用。与其他模式(Pattern)的合作在《P级阵地》的BattleState类实现中,分别调用了PBaseDefenseGame类的不同方法,此时的PBaseDefenseGame使用的是单例模式(Singleton),这是一种让BattleState类方法中的程序代码,可以取得唯一对象的方式。而PBaseDefenseGame也使用了外观模式(Facade)来整合PBaseDefenseGame内部的复杂系统,因此BattleState类不必了解太多关于PBaseDefenseGame内部的实现方式。状态模式(State)的其他应用方式:? 角色AI:使用状态模式(State)来控制角色在不同状态下的AI行为。? 游戏服务器连线状态:网络游戏的客户端,需要处理与游戏服务器的连线状态,一般包含开始连线、连线中、断线等状态,而在不同的状态下,会有不同的封包信息处理方式,需要分别实现。? 关卡进行状态:如果是通关型游戏,进入关卡时通常会分成不同的阶段,包含加载数据、显示关卡信息、倒数通知开始、关卡进行、关卡结束和分数计算,这些不同的阶段可以使用不同的状态类来负责实现。
|
|