LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

游戏编程模式②(观察者模式,原型模式)

2021/8/17

本文作为《Game Programming Pattern》(游戏编程原理)的摘要总结,记录为期一个月的阅读感想与心得体会。

撰写本文时,希望可以尽量遵循star法则,此后的文章同样以这四个要点作为大纲。

situation(情境)

task(任务)

action(行为)

result(结果)

本文代码尽量以unity c#进行演示(最近看的都是cpp代码,时常有被带偏的情况,本人实在太菜菜子了)

观察者模式

在对象间定义一种一对多的依赖关系,以便当某对象的状态改变时,与他存在依赖关系的所有对象都能收到通知并自动进行更新。

观察者模式是最为人所知的设计模式之一,使用最为广泛。我们可以用它来实现成就系统(成就要要检测敌人死亡个数,被观察者就是敌人死亡,观察者就是成就系统。)。

在c#中观察者模式被集成到了语言层面(event)

situation

将一个系统分割成一个一些类相互协作的类有一个不好的副作用,那就是需要维护相关对象间的一致性。


task

我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便。


action

我们先写一个抽象观察者类

public abstract class Observe {
    //得到通知后的响应
    public abstract void onNotify(const Entity entity,Event event);    
}

任何实现这个接口的具体类都会成为一个观察者。我们针对这个抽象类进行编程。

我们来完成一个成就系统吧

public class Achivement:Observe
    {
    public override void onNotify(const Entity entity,Event event)
    {
        switch(event)
        {
            case EVENT_1:
                {
                    ...();
                    break
                }
            ...        
        }  
}

成就系统作为观察者,能在被观察者更新时收到通知,任何进行更新。

我们接下来编写被观察者Subject(这里我们创建一个观察者集合,使用委托来写观察者模式可能会更方便些,但使用集合可能更通用)。

public abstract class Subject
{

    //观察者集合
    protected List<IObserver> mObserverList;

    public ISubject()
    {
        mObserverList = new List<IObserver>();
    }
    // 添加观察者
    public void AddObserber(IObserver observer)
    {
        //让我们检查一下吧
        if (mObserverList.Contains(observer) == false && observer != null)
        {
            mObserverList.Add(observer);
            return;
        }
        Debug.Log("报错");
    }
    // 移除观察者
    public void RemoveObserber(IObserver observer)
    {
        if (mObserverList.Contains(observer))
        {
            mObserverList.Remove(observer);
            return;
        }
        Debug.Log("报错");
    }
    // 通知所有观察者更新
    protect void NotifyObserver()
    {
        foreach (IObserver item in mObserverList)
        {
            item.onNotify();
        }
    }
}

我们的被观察者类完成了两个职责。一是它维护一个观察者列表,这些观察者随时等待通知。二是被观察者对象的NotifyObserver方法能够发送通知,调动观察者内的onNotify进行更新,同时我们也保护了NotifyObserver方法。

我们接下来只要使我们所需要的系统和Subject挂钩,就能将其登记到我们的成就系统上。我们可以调用NotifyObserver来发送通知,而任何地方都能调用添加和移除观察者的方法。


result

我们成功降低了耦合度,同时也满足开闭原则。

我们的c#有垃圾回收机制,所以可以不需要管理动态内存分配,一切都很合理。

与此同时我们得注意,观察者模式是同步的,这意味着所有的观察者得到通知后才能继续工作,而任何一个观察者对象都可能阻塞被观察者,我们需要更小心的处理线程。

我们还得注意及时清理例如死去的敌人那样失效的观察者。

观察者只能被动的获取数据,并不能主动的获取到想要的数据。因此它适合一些不相关的模块之间的通信问题,而不适合按个紧凑的模块。


原型模式

使用特定原型实例来创建特定种类的对象,并通过拷贝原型来创造新的对象。


situation

假设我们为游戏里面用一个抽象Monster类开始进行编程,那么可能遇到很多种继承它的敌人,人类、动物、龙,恶魔,各种等等。如果我们想为每个敌人做一个生成器父类Spawner,也会有与monster对应数量的子类。

这样就会产生类的数量很多,而且这些类的功能是重复的。这样造成了大量的冗余。(除非你的薪资以代码行数来计算)


task

我需要代码能够更加简洁。


action

为了解决最重要的冗余问题,我们先设计最开始的Monster,它有一个抽象方法clone。

public class Monster  {
    public string MonsterName;
    public int attack;
    public int defense;
    public string weapon;


   public abstract Monster clone();
}

C#数据类型大体分为值类型(valuetype)与引用类型 (referencetype)。

对于引用型变量而言,复制时,其实只是复制了其引用。复制引用的方式叫浅复制,

如果你想浅复制clone函数为:

  public Monster clone()
    {
        return this;
    }

它返回它本身的引用

若要深复制,就Instantiate一个新的:

  public  GameObject clone()
    {
        return Instantiate(this.gameObject) as GameObject;
    }

回归正题!让我们来看看子类吧

public class Dragon : Monster
{
    public Dragon()
    {
        MonsterName = "Dragon";
        attack = 8;
        defense = 15;
        weapon = "tooth";
    }
    public override Monster clone()
    {
        return new Dragon(a,b,c,d);
    }

}

只要让所有的这些子类都实现这些接口,我们就不需要单独再为每一个monster子类定义一个他们的spawner类了。

我们只需要定义一个spawner类:

public class Spawner 
    Monster mPrototype;
   public Spawner(Monster Prototype)
   {
       ...
   }
   public void setPrototype(Monster prototype)
    {
       ...
   }
    
  public  GameObject createMonster()
    {
        return prototype.clone();
    }
}

使用时只需要:

spawner.setPrototype(A);
spawner.createMonster();
spawner.setPrototype(B);
spawner.createMonster();

我们也可以通过实例化spawner来创造各种各样的生成器。

这样就能创造出风龙水龙土龙大龙的生成器啦。


result

实际上我们并没有节省多少代码量,因为各种各样的怪物的clone方法可能也不一定相同。

为每种怪物编写clone方法和实现单独的生成器其实差不了多少,但起码我们让自己的逻辑更加清晰了,代码也变得更加优雅。

我们需要注意深拷贝和浅拷贝的问题,否则编写出来的clone方法可能是有问题的。

如果我们想把代码变成实在的数据也是很方便的,就比如Monster里的属性都是相同的,我们只需要一个Monster类和一个存了各种敌人的数据的文件就行了。

我们通过序列化和反序列化对原型进行数据建模,再将其传递给spawner。这在数据量大的游戏中尤为明显,能省去大量代码。使用JSON,XML,二进制等等都可以,这样也便于实现游戏的存档读档功能。我更喜欢json,因为它更易读。


2021.8.16–19:04完成