目录json
游戏类型:塔防+RPG的3D游戏架构
游戏要素:3D 塔防 英雄 建筑树 搭配函数
主体玩法:游戏里将会有一波波怪物进攻基地。玩家能够建造塔来防护敌人,同时也能够控制单独的个体英雄角色来攻击敌人。工具
游戏模式:this
控制方式:在游戏中使用Tab按键,切换这两种操做模式:编码
胜利条件:消灭全部敌人 或者 坚持到时间结束插件
失败条件:基地生命值为0 或者 英雄死亡架构设计
通常来讲,整个Unity游戏项目总体结构,我比较偏向分为以下5部分:设计
场景对象: 不会产生互动的可视物体对象,例如地型/建筑/灯光。调试
游戏对象: 参与互动的游戏对象,例如英雄/怪物/塔。
游戏逻辑: 负责控制游戏的逻辑,其逻辑对象通常是单例的。
非游戏性对象: 负责加强游戏效果,但不是直接的游戏逻辑,例如UI/HUD/特效/声音。
工具: 负责辅助编码,例如日志工具,调试工具。
在《ATD》游戏项目里,我是这样设置游戏对象目录的:
注:“个体”在《ATD》里的术语表示游戏对象单位。
经过分析《ATD》策划案,确立了两种基本游戏机制:
和策划商量后,策划制做了下面一张含全部Buff属性的Excel表:
因为策划还没想好Buff名字,直接套用装备或者技能名字来命名Buff。
首先,使用了一个数据类型BuffData,用于彻底映射Buff在表格的全部属性:
public class BuffData { public int ID; public string Name; public int HpChange; //血量变化 public double HpChange_p; //血量百分比变化 public int AttackChange; //攻击力变化 public double AttackChange_p; //攻击力百分比变化 public double AttSpeedChange_p; //攻击速度百分比变化 public double SpeedChange_p; //速度百分比变化 public int HpReturnChange; //血量恢复数值 public double HpReturnChange_p; //血量百分比恢复数值 public int AddReviveCount; //增长复活次数 public bool isDecelerate; //减速 public bool isVertigo; //眩晕 public bool isParalysis; //麻痹 public bool isSleep; //睡眠 public bool isBound; //束缚 public bool isBurn; //点燃 public bool isCharm; //魅惑 public bool isIncreaseAttSpeed; //攻速提升 public bool isPoisoning; //中毒 public bool isImmuneControl; //免疫控制 public bool isRevenge; //复仇 public bool isTaunt; //嘲讽 public bool isIncreaseHpReturn; //回血速度提升 public bool isIncreaseAttack; //攻击力提升 }
而后咱们就能够用一个List
//全局单例类 public class BuffDataBase : MonoBehaviour { //读取excel插件生成的json文件 public TextAsset BuffDataJson; //存储BuffData的列表 private List<BuffData> buffDatas; //全局单例实现 //... //根据ID获取相应的BuffData对象 public BuffData GetBuffData(int ID){ //... } }
为了表示游戏对象动态获得/失去一个Buff而从BuffDataBase找到对应并拷贝一份BuffData对象/释放掉一份BuffData对象显然是不明智的。(BuffData所占空间大,开销大)
正确的作法应该是使用索引/引用的方式,例如某个游戏对象持有3号索引,则表示它当前受一个3号Buff影响。
为了引入Buff的时间有效性,则进一步封装索引,因而编写了下面一个Buff类:
public class Buff { public int ID; //BuffData的ID(索引) public double time; //持续时间 public int repeatCount; //重复次数 public bool isTrigger; //是否触发类型 }
由于每一个Buff的时间有效性都有所不一样:有些Buff是一次性触发Buff;也有一些是持续性Buff,持续N秒;还有一些是被动buff,永久生效。
因此我这里就总结了个规则,Buff主要分为两种类型:
而后Buff的有效时间取决于2个属性:
当一个Buff对象,持续时间 <= 0 而且 触发次数为0,则应视为失效。特殊地,触发次数为-1时,表示无限时间。
这样Buff/BuffData/BuffDataBase基本构造就这样了:
整个游戏同种类Buff只用存储一份BuffData;可是能够有不少个对象持有索引/引用,指向这个BuffData
游戏对象持有Buff对象,经过BuffDataBase访问BuffData的数据,而后利用这些数据对游戏对象属性形成影响
看到这里,可能会有人想到前面有个问题:对于任意一种Buff,它每每有不少属性是false或者0,使用这种彻底映射会不会很影响空间占用或者效率。
首先,空间占用绝对不用担忧,由于前面BuffDataBase机制保证同种Buff只有惟一BuffData副本,其全部BuffData总共占用量不过几kb而已。
其次,至于效率,例如说某个Buff对某个游戏对象形成影响,由于是彻底映射,因此须要对该游戏对象每一个属性都要进行更新,其实这也并非太糟糕。并且只要游戏对象有比较好的Buff计算方式,可让一个Buff对象的整个有效周期只对对象形成两次影响计算(生效影响,失效影响),避免每帧出现影响多余的计算,这样就很不错了。
能够说技能是我比较头疼的部分。
看到那千奇百怪的Skill需求时,而后才总结出大概这几个分类:
最后我决定使用继承接口的方式来实现Skill:
技能接口类:
public interface ISkill { // 技能初始化接口 void InitSkill(Individual user); // 使用技能接口 void ReleaseSkill(Individual user); /// 技能每帧更新 void UpdateSkill(Individual user); /// 技能是否冷却 bool IsColdTimeEnd(); // 技能冷却百分比 float GetColdTimePercent(); }
须要注意的一点是,技能并非主动释放时调用一个自定义的技能函数便可完事:
例如持续性的范围技能,须要每帧调用散发Buff的函数。
因此一个ISkill对象 该有这3种重要的接口方法:初始化/主动释放/每帧更新
下面是其中一个派生类的具体实现:
因为进度未完,目前只有两个派生类:Buff技能类和召唤技能类。
Buff技能类暂时包含了ActiveBuff技能类和PassiveBuff技能类的功能。
// 示例:Buff技能类 public class BuffSkill : ISkill { public int buffID; //目的Buff public bool isAura = true; //光环 public bool releasable = true; //是否主动释放 public float range = 0.01f; //范围 private float coldTime = 5.0f; //冷却时间 private float timer = 5.0f; //冷却计时 public BuffSkill(int buffID,bool releasable = true,bool isAura = true, float range = 0.01f) { this.buffID = buffID; this.isAura = isAura; this.range = range; this.releasable = releasable; } public void InitSkill(Individual master) { if (!releasable && !isAura) { var individual = master.GetComponent<Individual>(); master.GetComponent<MessageSystem>().SendMessage(2, individual.ID,buffID); } } public void ReleaseSkill(Individual master) { if (releasable && IsColdTimeEnd()) { timer = 0.0f; Factory.TraversalIndividualsInCircle( (individual) => { master.GetComponent<MessageSystem>().SendMessage(2, individual.ID, buffID); } , master.transform.position, range); } } public void UpdateSkill(Individual master) { //增长计时 timer =Mathf.Min(timer+Time.deltaTime, coldTime+0.1f); if (!releasable && isAura) { Factory.TraversalIndividualsInCircle( (individual) => { master.GetComponent<MessageSystem>().SendMessage(2, individual.ID, buffID); } , master.transform.position, range); } } public float GetColdTimePercent() { if (!releasable) return 1.0f; return timer / coldTime; } public bool IsColdTimeEnd() { return timer > coldTime; } }
派生类的构造函数很重要,这样即便硬编码了4个技能派生类,经过不一样的数据参数传入,也能产生更多不一样的技能对象。
最后还应该再写一个SkillDataBase全局单例类,它负责读取策划写的技能配置文件,来初始化出来一些Skill对象,以供游戏对象使用。
不过项目代码还没写完,所以目前是直接在SkillDataBase的初始化函数直接硬编码3个技能。
//TODO //目前硬编码给玩家赋予3个技能 HeroSkills.Add(new BuffSkill(6, true, true, 5.0f)); //主动技能:嘲讽Buff HeroSkills.Add(new BuffSkill(0, false, false)); //被动技能:回血buff HeroSkills.Add(new BuffSkill(14, true, false)); //主动技能:攻速戒指buff
之后的话,SkillDataBase的初始化函数应该是读取某种配置文件,而后生成若干个对应的技能对象分配给游戏对象使用:
《ATD》只是社团部门内提出的一个游戏项目,而我负责这个项目的程序架构设计,然而中途开发由于很多事,咱们不得不放弃了这个项目。所以才想写点东西总结一下开发这个项目时的经验。
以后还会有新博文来更新这个系列,大概涉及《ATD》的游戏对象模型,全局游戏逻辑,UI/HUD/特效/声音管理,工具等,也同时会分享一些trick。