最近玩了不少三角洲,对游戏机制的实现有一点自己想法的猜测,虽然不一定对但是通过这个游戏对一种设计模式进一步了解也是有帮助。
策略模式 是一种行为设计模式,它允许你定义一系列算法或行为,并将它们封装在独立的类中,使得它们可以互相替换。策略模式让算法的变化独立于使用它的客户端。
其核心思想是:将具体的策略抽象一层,提炼出共性,成为接口,再使用具体的类去实现提炼出的接口,这些具体的类就是我们实际的策略。最后最关键的一句话是客户端持有一个接口的引用,并在运行时根据条件动态切换策略。
虽然以上这段话比较关键,但是基本属于懂的人不需要看,不懂的人看了也不懂的程度。
所以借用三角洲这个游戏,具体解释一下上面的这段话。
我们以三角洲这种摸金游戏最关键的地图物资刷新这件事为切入口
三角洲这b游戏当你第一次使用3*3保险箱时,肯定大概率出现一次九格大红让你体会一把3*3保险箱的爽局,方便等保险箱过期了诱惑你充钱再续保险箱。当你破产时地图也会更容易刷出高价值物品扶贫。当你打的比较好的时候就很难出大货,你连续死几把也会出现所谓”补偿局“等等情况。那么以上策划设计的系统就可以看做地图刷新物资的不同策略,就以我刚刚说的内容,我们可以区分出
- 正常刷新策略
- 玩家破产刷新策略
- 高压局策略
- 补偿局策略
策略脑子一拍搞这么多花里胡哨的设计出来了,那么程序应该怎么实现?程序最该给这群最爱脱裤子放屁要不然无法证明自己存在价值的策划两巴掌让他别整这些有的没的,每一把就正常玩就行了,玩家自己会自己调整打法,每一把玩家怎么知道被你设计到什么策略里去了,到底该怎么决定这把的打法?
骂策划归骂策划,下面按介绍完的背景,还是要说说具体实现方法。
按照上文核心思想的第一步:“将具体的策略抽象一层,提炼出共性,成为接口”
public interface IMapRefreshStrategy
{
void ChangeRate(float rate1, float rate2);
void GenMap();
}
不同策略的本质目的是控制地图物资刷新的价值,简单点考虑就用高低价值物品刷新率表示,这就是所有策略的共性。
第二步:使用具体的类去实现提炼出的接口,这些具体的类就是我们实际的策略
//正常刷新策略,高级物品低级物品五五开是默认情况
public class NormalStrategy : IMapRefreshStrategy
{
float highGradeRate=0.5;//高级物品刷新率
float LowGradeRate=0.5;//低级物品刷新率
public void ChangeRate(float rate1,float rate2)
{
highGradeRate=highGradeRate*rate1;
LowGradeRate=LowGradeRate*rate2;
}
public void GenMap()
{
//使用highGradeRate和LowGradeRate作为参数生成地图
//.........
}
}
public class PlayerBankruptcyStrategy : IMapRefreshStrategy//破产局策略
{
float highGradeRate;//高级物品刷新率
float LowGradeRate;//低级物品刷新率
public void ChangeRate(float rate1,float rate2)
{
highGradeRate=highGradeRate*rate1;
LowGradeRate=LowGradeRate*rate2;
}
public void GenMap()
{
//使用highGradeRate和LowGradeRate作为参数生成地图
//.........
}
}
public class CompensateStrategy : IMapRefreshStrategy//补偿局策略
{
float highGradeRate;//高级物品刷新率
float LowGradeRate;//低级物品刷新率
public void ChangeRate(float rate1,float rate2)
{
highGradeRate=highGradeRate*rate1;
LowGradeRate=LowGradeRate*rate2;
}
public void GenMap()
{
//使用highGradeRate和LowGradeRate作为参数生成地图
//.........
}
}
注意上述各种策略都是单独的类,实际在Unity的实际项目中每个类可能作为单独.cs文件存在,这一点在实际项目中看到了记得辨别。
以上都是常用的对通用功能进行简单封装的操作,并不涉及到什么设计模式,你多写几个重载函数或者就是搞个继承也能实现。
下面一个概念就是策略模式的关键概念并起到关键作用了:上下文类
上下文类持有接口的引用,并在运行时调用具体策略。该类存在的必要也是最关键的一句话是上下文类持有接口的引用。
public class MapContentGen
{
private IMapRefreshStrategy _mapRefreshStrategy
// 设置策略,类似构造函数
public void SetMapRefreshStrategy(IMapRefreshStrategy mapRefreshStrategy)
{
_mapRefreshStrategy= mapRefreshStrategy;
}
// 改变地图物品倍率
public void ChangeMapRate(float rate1,float rate2)
{
_mapRefreshStrategy.ChangeRate(rate1,rate2);
}
public void GenerateMap()
{
_mapRefreshStrategy.GenMap();
}
}
比起普通的类直接实现接口后在类自己“体内”使用接口定义的函数,策略模式的区别就在于这个上下文类了,上下文类多套了一层,把接口作为自己的成员变量持有,并在后续使用,那么策略模式的优点和必要性也就在这个特殊之处了,后文会分析一下为什么要一层套一层的搞这么多设计模式。
最后我们终于可以在游戏循环的主逻辑调用我们做的这么多铺垫工作,真正根据不同的策略自动切换不同的地图高级物品的生成概率
//游戏主循环,每一局或者每时每刻检测玩家当前各项游戏数据判断该玩家属于哪一类玩家
class Program
{
static void Main(string[] args)
{
MapContentGen mapContentGen= new MapContentGen();//上下文类负责总控策略的切换
if(player=="正常玩家")
{
mapContentGen.SetMapRefreshStrategy(new NormalStrategy());//普通模式
// 对高级物品刷新概率和低级物品刷新概率不做改动
mapContentGen.ChangeMapRate(1,1);
mapContentGen.GenerateMap();
}
if(player=="破产玩家")
{
mapContentGen.SetMapRefreshStrategy(new PlayerBankruptcyStrategy());//破产模式
// 大幅度增加高级物品刷新概率和降低低级物品刷新概率
mapContentGen.ChangeMapRate(1.8,0.2);
mapContentGen.GenerateMap();
}
if(player=="补偿玩家")
{
mapContentGen.SetMapRefreshStrategy(new CompensateStrategy());//补偿模式
// 小幅度增加高级物品刷新概率和降低低级物品刷新概率不做改动
mapContentGen.ChangeMapRate(1.2,0.8);
mapContentGen.GenerateMap();
}
}
}
这里有一个语法上的写法值得注意
mapContentGen.SetMapRefreshStrategy(new NormalStrategy());
这个函数中参数传的是NormalStrategy类的一个对象,但是该函数定义时
public void SetMapRefreshStrategy(IMapRefreshStrategy strategy)
其参数是一个接口的对象,这么写没有问题吗?
完全没问题
new NormalStrategy() // 这是一个具体实现类
IMapRefreshStrategy // 这是它实现的接口
面向对象设计的核心原则之一,里氏替换原则要求"子类型必须能够替换它们的基类型"
在此例中则意味着
-
所有接受
IMapRefreshStrategy
类型参数的地方 -
都可以安全地传入它的具体实现类(
NormalStrategy
/PlayerBankruptcyStrategy
/CompensateStrategy
)
这也是策略模式必须要的写法,如果写死某一种策略作为函数参数,那么也失去了动态替换的功能了。
该设计模式的类图如下所示
相比于简单的继承,为什么一定要用这个设计模式有什么优点?
除去依赖倒置,开闭原则这些抽象的概念,其中最主要的的优点就是使用该策略模式可以在游戏运行中根据玩家状态(如从正常变为破产)即时更换策略对象,而继承和重载需要重新初始化对象,要不然就需要在Update方法不断判断玩家当前状态再根据if去切换调用的函数,显然在Update里做轮询操作是大多数主程或项目不允许的,实际工作中这样基本是不行。
另外一个优点是我们在游戏主逻辑中实际上只操作上下文类,而上下文类仅依赖接口,不关心具体策略实现,降低模块间耦合度。
对于Unity来说,实现一个策略类就更简单了,我们可以创建一个空的gameObject,把每一个策略类单独写成一个cs文件,在游戏运行中动态的去我们创建的gameObject上挂载不同的策略类文件就行了,这个gameObject就起到上下文类的作用,游戏主逻辑只获得gameObject的引用,相较于完全抽象的逻辑,在Unity中有一个gameObject实体更容易理解。
另一个好处是得益于Unity的生命周期机制,我们动态的把不同策略类脚本作为component直接add上一个不销毁的gameobject,这样我们可以再简化一下上下文类的内容,不在需要定义或者调用设置策略的函数了。
public class MapContentGen
{
private IMapRefreshStrategy _mapRefreshStrategy
// 设置策略,类似构造函数
//public void SetMapRefreshStrategy(IMapRefreshStrategy mapRefreshStrategy)
//{
// _mapRefreshStrategy= mapRefreshStrategy;
//}
// 改变地图物品倍率
public void ChangeMapRate(float rate1,float rate2)
{
_mapRefreshStrategy.ChangeRate(rate1,rate2);
}
public void GenerateMap()
{
_mapRefreshStrategy.GenMap();
}
}