利用一个需求对比两者,并说明什么是数据驱动?为什么在某些场景下组合优于继承?
一、需求例子
设计消除类游戏的方块,
方块可能有普通方块、不可被移动方块、不可被消除方块、既不能被移动也不能被消除方块、相邻方块消除后消除自己的方块、自己消除后会发射火箭的方块等等,。
二、用接口、继承、多态那一套来做是怎么样
- 接口用来规范行为,定义能力
比如:
cs
public interface IMovable
{
bool canMove();
}
那么继承这个接口,实现方法,方法内容是判断方块是否可以被移动。
- 实现一个抽象类,实现共享的公共方法和属性
cs
public abstract class BaseBlock : IMovable,...其他接口
{
// 公共属性
// 比如位置信息、配置信息等等。
// 实现ICanNotMove方法
public virtual bool CanMove()
{
}
// ... 其他接口的方法
}
-
实现具体的类型方块,通过继承BaseBlock,多态
public class NormalBlock : BaseBlock
{
// 子类自己的特别属性//实现自己的逻辑 public override bool CanMove() { return true; }}
public class CanNotMoveBlock : BaseBlock
{
// 子类自己的特别属性//实现自己的逻辑 public override bool CanMove() { return false; }}
每需要一个新方块几乎都要创建一个新的子类,子类爆炸。
-
移动管理模块,访问方块BaseBlock或者ICanNotMove,判断是否可以移动
// 移动管理模块
// 通过方块管理类拿到这个Block
BaseBlock block = BlockManager.instance.GetBlock(pos);
bool blockCanMove = block.CanMove();
if(blockCanMove)
{
// 移动逻辑
} -
阶段总结
我们发现这种开发模式的一个特点,那就是每需要一个新的方块,就需要开发者用代码开发出一个新的方块类,开发新的接口。
也就是说创建新方块类型的操作在程序员这里。
三、利用数据驱动思想,将创建方块的操作从程序员代码创建移交给策划。
- 定义配置文件,配置就是数据
cs
public class BlockType:ScriptableObject
{
public int id;
public string Desc;
// 基础属性
public bool canMove = true;
public bool canClear = true;
// 为了防止属性过多,将大块属性分到各个模块
public List<BaseBehaviour> behaviours;
}
public abstract class BaseBehaviour : ScriptableObject
{
// 当被消除调用
public virtual void OnClearBlock()
{
}
}
public class FireRocketBehaviour : BaseBehaviour
{
public int FireCount = 2;
// .. 其他属性
public override void OnClearBlock()
{
// 火箭管理系统发射火箭
FireRocketManager.Fire();
}
}
- 创建一个方块的操作,就交给了和程序无关的人
直接创建这个Scriptobject,填写信息、勾选基础能力、添加模块能力,就组合出了一个新的方块。
方块可以同时拥有配置中的多种能力,这些能力任由策划配置驱使。
- 开发做什么?
开发要在游戏逻辑中,实现每个能力的效果和先后顺序。
针对配置的canMove 字段,就要在移动阶段判断,然后做相应操作。
针对配置的canClear字段,就要在消除阶段判断,然后判断是否能将其删除。
程序从既要用代码创建模块还要实现能力->转变为专注地实现能力,创建交给配置和数据。
继承是"类型决定能力",组合是"能力拼出类型"。
有 N 种能力,就能组合出 2^N 种玩法,而不需要创建 2^N 个类。