[RTS] 远古帝国
游戏链接https://pan.baidu.com/s/1eSd4ZHg
这是我开发的一款策略战棋游戏[远古帝国]
他是我以前上学时经常玩的一款游戏 虽然现在市面上有很多重置版但是我还是喜欢玩原版 因为里面的战斗数据非常平衡 你能从中获得更多的乐趣
所以我尽量还原经典 仔细分析数据 力求完美(上下左右是wsad或方向键控制 确认键是空格或回车)

首先是移动范围的渲染

这里的人物移动范围可以在ACGame.Sprite中的MoveRange里配置

然后是地图 这张大地图都是由15*15的小地图拼接起来的

每张小地图上都挂了ACGame.Terrain脚本
CustomDepth设置的是移动到该地形需消耗的移动点
Def则是角色在该地形上的防御加成

当玩家点击了目的地 角色便会沿着路线移动过去


当角色移动完毕会跳出一个提示框显示是否结束当前移动
如果角色移动的目的地中有敌人则会出现攻击选项

每个角色的攻击范围不同 可以在脚本中配置

如果角色附近有多个敌人 玩家可以按上下左右键选择要攻击的敌人

然后就可以开始攻击

玩家攻击完成后敌人也会反击

角色的攻击力是一个范围 在脚本中可以配置 每次会从中随机取一个值

攻击的计算公式是(攻击方的攻击力-敌人防御-地形防御加成)*攻击方的血量百分比
这样可以保证攻击时血量较多的一方比较有利
玩家也可以利用山地 房屋等有防御加成的地形攻击敌人
也可以先使用满血的单位攻击 然后再使用残血单位补刀如果玩家或者敌人血量为0 则会爆出一块墓碑

接下来是购买界面

点击下方左右箭头可以在各个兵种之间切换 这里实现了无缝滚动
然后是我的代码
代码一共分为两大类 分别叫abstract和business
abstract中存放的都是接口 抽象类 和工具类
它们不依赖于business 所以不仅可以运用于当前项目
也可以在其他项目中使用
business存放具体的业务逻辑 这里的代码依赖关系比较复杂
这样的分类也是我研究了好久 我曾经试过将所有的类都面向接口 耦合也降到最低
但是这样做没有意义
因为就算面向了接口 但最终还是要把各处代码组合在一起 耦合还是存在
并且修改的话还是有可能修改好几处地方
后来我取消了接口 又发现代码变得更加难以维护
最后我做了分类 因为既然我没办法把所有的类全部独立 也不能把所有的类依赖在一起
那我就索性把独立性较强的类和处理业务逻辑的类分开
这样就诞生了abstract和business而在实际编程中经常会出现business的代码写到一半发现之前写过
然后把重复的逻辑整理好放到abstract中
而放入abstract中的代码基本不会再去放回business

在abstarct中有一个分类是专门存放设计模式的
虽然里面就两个类 但代码中远不止这两种设计模式
放在这里仅仅是因为它们整个类里都在做设计模式做的事
而且其他模式则和其他逻辑耦合在一起

首先是DynamicFactroy
他本质是一个工厂模式 但是没有一本书上会有这种工厂模式 这是我自创的
这里就做两件事 先找字典中有没有你要的对象 没有就用反射创建一个

第二个是单例模式
我知道学过c++的人都会七八种单例模式的写法
但是这里我就按照unity的思路写一个就行了

还有一个非常棘手的问题就是对象的初始化参数
按照我们以前的思路 要么把类中需要初始化的数据在声明时就赋值
要么写一个构造器 但是在unity中
如果你的类继承了MonoBehaviour你就没办法使用有参构造器了
因为unity要序列化你的脚本显示在inspector上 它走的是无参构造器 就算你写了有参构造器他也不会走
而且会报警
我看到很多老外的代码里直接写了个setter 我觉得这样做违背了初始化这个词的两个初衷
首先破坏了封装 你的内部变量可以在对象创建后随意修改
其次有空数据的风险 如果你的代码没有调用相关setter
编译不会察觉 那你的变量就为null了
这里我的解决方法是写一个ResourceManager 你可以看到它使用了我的单例模式

然后在脚本执行顺序中把他放到第一个

这样我就可以在代码中的任何地方随意调用初始化好的参数

但是你可以看到ResourceManager中有各种各样的控件
光标 相机等
我的项目有这些控件 而你的项目不大可能也有这些
所以ResourceManager应该归到business中
那如果ResourceManager是business了 那么abstract中继承了MonoBehaviour的类也想要初始化参数怎么办
这里还有一个办法
就是在这些类中写一个抽象函数作为构造函数这里拿一个放大特效脚本做例子它的效果是激活后 物体会从一个点慢慢的放大成原来的样子
接下来说说状态模式 里面存放的全是游戏业务逻辑
如果你按下一个键后需要一大堆if else 那么你就需要状态模式
我发现HeadFirstParttern一书中的状态模式有些不足
第一 他的状态会频繁的创建
第二 他的状态切换必须经过Update
而我的状态对象的获取是通过我的动态工厂获得的 同一个状态只有一个对象
然后我的状态切换并不是通过update切换的
而是通过GoNextState<T>()
这样我可以在任何事件中切换状态
我的状态模式基类

状态子类 可以看到当子类中触发了点击事件后会进入MoveState

进入MoveState后角色移动完毕又进入BehaviourState

在BehaviourState中玩家如果选择移动结束则回到InitialState 如果选择攻击 则进入SelectEmyState

在这个项目里一共有八个状态

他们之间的切换过程可以在inspector中可以看到

这里出现了一个GameManager 起始它的作用就是刷新游戏状态
这样我的游戏中只有一个update在运行

接下来是最困难的地方 就是地形的遍历
作为一个策略战棋游戏 最必不可少的就是地图遍历
这里我使用了四叉树
而这里的四叉树和一些算法书上不一样 一般数据结构的节点都会先建立一个四叉树节点类
然后在类中存一个泛型数据 但我觉得这样用的时候不方便
所以我的写法是每一个节点都包含了它上下左右四个同类型的节点

设计好了四叉树 那么在遍历前先要把他们连接起来
这里我写个接口IQuadNodeSetter用于设置四叉树的四个节点

然后是定义位置接口IPositional 因为我想通过物体的位置关系来连接

连接之前我会对所有的地形根据位置进行排序 所以还要用到IComparable<IPositional>
所以一个节点连接的所有接口准备齐了 把它们合在一起就是ILinkable接口

任何类只要实现了ILinkable接口就可以在PLinkQuadNode中被连接成四叉树

在unity的界面中是这样的

点击连接按钮它就会连接子节点中所有实现ILinkable的节点

你可以看到我并没有开始运行游戏 而Terrain中已经有了值
这样我就可以在游戏运行之前就连接好四叉树 不会在游戏中消耗性能
所以PLinkQuadNode中的P表示的是Preprocess
凡是这种脚本都放在Preprocess文件分类中有了连接好的四叉树
那么就可以遍历了
这里我写了三种遍历器 而且每种遍历器都使用了策略模式和享元模式

第一个构造器中使用的IIterateBehaviour接口用来定义遍历时的行为

先实现接口

再把他们组合起来

最后呈现出来的效果

如果改变遍历时的行为

就会这样渲染

这里我还可以使用相同的遍历行为 但使用不同的遍历方法 这样就从策略模式变成桥接模式了




这里我测试了遍历的逻辑


我建立了一个测试文件夹 里面存放test代码

每次遍历到一个节点 就把它z轴提高
这样我可以清除的看到是否有重复遍历

最后是享元模式 每个遍历器都可以选择创建新的变量 也可以公用其他遍历器实例的变量