根据哈基米的推荐买了一本Game Programming Patterns,准备系统的学习一下,边学边把自己的理解写下来(
看了一下目录,正如其名,这本书基本都再说一些设计模式。之前虽然接触学习过不少的设计模式,但是游戏中经常会用到一些特定的设计模式,所以还是很有必要系统的学习一下的。
当然,还是以很常见的设计模式为基础的啦
1.命令模式
命令模式的核心思想是把一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
这个模式还是很常见的。之前写RPMEditor的时候,这种软件的操作需要用到很多命令模式。甚至说WPF本身就提供了一个ICommand接口。
一个最简单的例子就是对玩家输入的监听。
[!Note] 原书用的cpp写的代码,但是我不太熟( 所以用kotlin写了
typealias Command = () -> Unit
// 将行为和命令绑定在一起var buttonX: Command = ::jumpvar buttonY: Command = ::attackvar buttonA: Command = ::swapWeaponvar buttonB: Command = ::useItem
fun handleInput(){ if (input.isPressed(BUTTON_X)){ buttonX() }else if (input.isPressed(BUTTON_Y)){ buttonY() }else if (input.isPressed(BUTTON_A)){ buttonA() }else if (input.isPressed(BUTTON_B)){ buttonB() }}2.享元模式
享元模式的核心思想是运用共享技术有效地支持大量细粒度的对象。
看起来可能会和单例模式有一点相似的地方,但是享元模式侧重于共享对象而减少多余对象的创建,单例模式一般用于全局唯一的对象,保证一致性。
享元模式的一个经典例子就是在游戏中对树木的渲染。假设我们有一个森林场景,其中有成千上万棵树,如果每棵树都创建一个独立的对象来存储它们的属性和状态,那么会占用大量的内存资源。但是这些树木往往共享了相同的纹理、模型和行为,我们可以通过享元模式来共享这些公共的部分,从而减少内存的使用。
class TreeType(val name: String, val texture: Texture, val model: Model)class Tree(val x: Int, val y: Int, val type: TreeType)class TreeFactory { private val treeTypes = mutableMapOf<String, TreeType>()
fun getTreeType(name: String, texture: Texture, model: Model): TreeType { return treeTypes.getOrPut(name) { TreeType(name, texture, model) } }}享元模式将单个的对象分为了两部分——每个对象自己的状态,以及所有对象之间共享的数据,或者说和上下文无关的状态,从而减少内存的使用,或者节约向GPU发送数据的开销。
3.观察者模式
观察者模式可以说是应用得最广泛的设计模式了。它的核心思想是定义对象间的一种一对多的依赖关系,以便当一个对象改变状态时,所有依赖于它的对象都得到通知并被自动更新。
比如说,我们要在玩家物品栏发生变动的时候,检查物品栏的物品,并决定要不要弹出成就。我们当然不会中每个可能修改玩家物品栏的地方都写一个checkItems,相反,我们可以在玩家物品栏发生变动的时候,发出一个事件,所有需要监听这个事件的地方都可以订阅这个事件,从而在事件发生的时候得到通知。
class Inventory { //一个简单的物品栏 private val items = mutableListOf<Item>()
//简单的观察者列表,我们不像原书里面自己实现一个复杂的列表,直接用ArrayList就好了 private val listeners = mutableListOf<() -> Unit>()
//我们不直接暴露列表,而是暴露操作列表的接口 //当列表被操作的时候,触发列表更改的事件,通知所有的观察者 fun addItem(item: Item) { items.add(item) notifyListeners() } fun removeItem(item: Item) { items.remove(item) notifyListeners() } fun subscribe(listener: () -> Unit) { listeners.add(listener) } private fun notifyListeners() { listeners.forEach { it() } }}
class AchievementSystem(val inventory: Inventory) { //简单的成就系统,对物品更改的事件进行订阅 init { inventory.subscribe { checkAchievements() } } fun checkAchievements() { // 检查成就 }}但是用一点需要注意的是,观察者往往会持有被观察者的引用。如果被观察者是一个需要被清除的非长期对象,比如说一个临时弹出的UI,那就需要注意观察者对事件订阅的取消了。不然,GC永远不会清理这些临时对象,很容易造成内存泄露的问题。
Java早期提供了一个观察者模式接口java.util.Observer,用于实现发布-订阅式的通知,但是它在Java 9中已经被标记为depercated。更现代的解决方案是PropertyChangeListener,适合JavaBeans属性变化通知。
PropertyChangeSupport pcs = new PropertyChangeSupport(this);
pcs.addPropertyChangeListener(evt -> { System.out.println(evt.getPropertyName() + " changed");});
pcs.firePropertyChange("name", "old", "new");或者使用Java 9+的Flow API。
当然,在C#中,事件作为语法功能被直接提供了。
public class Inventory { public event Action OnInventoryChanged;
public void AddItem(Item item) { // 添加物品逻辑 OnInventoryChanged?.Invoke(); }}4. 原型模式
原型模式的核心思想是用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式在游戏中经常被用来创建大量相似的对象,比如说敌人、子弹等等。通过克隆一个原型对象,我们可以快速地创建出新的对象,而不需要重新初始化它们的属性。
interface Prototype { fun clone(): Prototype}
class Enemy(val health: Int, val damage: Int) : Prototype { override fun clone(): Prototype { return Enemy(health, damage) }}原型模式一个很有意思的地方就是,它不需要类的定义,因为它总是将一个已有的对象作为原型,并以此生成新的对象。现代语言中一个经典的例子就是 javascript。javascript虽然拥有class关键字,拥有类定义的语法,但是它并不是传统的类继承为主,而是构造原型链来共享属性和方法的。
//定义了一个对象const parent = { say() { console.log("hi") } }//以parent为原型创造对象const child = Object.create(parent)
//child中并没有say,但是它的原型parent中有,所以child可以访问到say方法child.say() // hijavascript的函数中也有一个prototype属性,用来给通过new创建的对象共享方法:
function Person(name) { this.name = name}
Person.prototype.say = function () { console.log(this.name)}
const p = new Person("Alice")p.say()p的原型指向Person.prototype,方法say不会复制到每个实例,所有实例共享同一个方法。
在构造游戏中常用的Json数据的时候,原型思维同样适用。例如,我们需要构造多种剑,我们可以先构建一个剑的原型对象:
{ "name": "Sword", "damage": 10, "otherProperties": "..."}随后,例如,构造一把有技能的剑:
{ "prototype": "Sword", "name": "Sword of Fire", "skill": "Fire Blast"}这样子,我们只需要花费更少的精力,就能构造新的物品了。
继续看书喵,还有好多好多呢。
部分信息可能已经过时



