作为创新实训的项目,很重要的就是创新。我们首先为了写这个算法框架首先进行了工程上的创新,使用ECS框架。
ECS是什么?
这个框架十多年前可能就有了,但大概是守望先锋在GDC2016提出它后,变得非常流行起来,Unity2018也将采用ECS的编程规范。
首先我们先用文字来说说这是啥玩意。它规定你的游戏先有一个世界。这个世界下有若干实体和若干系统。每个实体由若干组件,每个系统在每一帧更新的时候将拿出系统中固定组件组合的实体,并更新它。而这也就组成了ECS的三个概念:Entity,Component,System。特别的,我们还规定Component里只能有数据和对数据的操作,System里只能有函数。
写到这,你大概就会发现他和面向对象思想的不同了。比如我们有玩家、怪物。若按照面向对象的思想,首先我们要写一个玩家类,玩家类下有玩家的数据和玩家的行为如:攻击、受伤、渲染等;还有一个怪物类,有数据和行为等。但如果是ECS来说呢,会有玩家、怪物两个实体,上面带着自己的攻击组件(附有攻击的数据),受伤组件(附有受伤的数据)。。。。会有攻击系统、受伤系统、渲染系统,根据是否有攻击组件、受伤组件、渲染组件来自动选择实体做操作,但并不care这是哪个实体,对于系统来说,只要有组件的,都一样。
ECS的优点:
虽然这是面向对象的框架,但不得不说,这种思想其实更像函数式编程。不把函数拘泥于一个类的行为,而是把执行过程看成系统(函数)对实体中组件(数据)的不断映射的结果。
好处仅说一些我个人的理解:每个系统功能单一,很好Debug,天生的就做好了接口隔离。用组合代替了继承,在ECS里,很少用继承,更多是不同组件和不同函数的组合,这极大的减少了代码量。而且对于大工程来说,一直继承其实是很崩溃的事。然后,好分工。。(对于我们这种不太熟悉团队开发的学生来说)
代码:
using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using UnityEngine; using System.Reflection; using System; namespace UnityECS { public class BitBunch { public bool[] mBitArray = new bool[200]; public int mMaxCount = 200; public int mCount = 0; public bool GetBit(int mx) { return mBitArray[mx]; } public void SetCount(int count) { if (count > mMaxCount) { mBitArray = new bool[count * 2]; mCount = count; for (int i = 0; i < mCount; i++) mBitArray [i] = false; } else { mCount = count; for (int i = 0; i < mCount; i++) mBitArray [i] = false; } } public void AddBit(bool mb) { if (mCount < mMaxCount) { mBitArray [mCount] = mb; mCount++; } else { mMaxCount *= 2; bool[] tBitArray = new bool[mMaxCount]; for (int i = 0; i < mCount; i++) tBitArray [i] = mBitArray [i]; mBitArray = tBitArray; mBitArray [mCount] = mb; mCount++; } } public void SetBit(bool res,int mx) { mBitArray [mx] = res; } public static bool Equal(BitBunch mbfir,BitBunch mbsec) { if (mbfir.mCount != mbsec.mCount) { return false; } for (int i = 0; i < mbfir.mCount; i++) { if(mbfir.GetBit(i)!=mbsec.GetBit(i)) { return false; } } return true; } public static BitBunch And(BitBunch mbfir,BitBunch mbsec) { BitBunch tBitBunch = new BitBunch (); for (int i = 0; i < mbfir.mCount; i++) tBitBunch.AddBit (mbfir.GetBit(i)&mbsec.GetBit(i)); return tBitBunch; } }; public class UEntity { public ECSWorld mWorld; public uint mWorldID; public BitBunch mAllBitBunch = new BitBunch(); public UComponent[] mUComponent = new UComponent[1024]; int mMaxComponentCount = 1024; public int mComponentCount = 0; public T GetComponent<T>() where T:UComponent { Type requestType = typeof(T); for (int i = 0; i < mComponentCount; i++) { Type tType = mUComponent [i].GetType (); if (tType == requestType) { return (T)mUComponent [i]; } } return null; } public void AddComponent<T>(T mT)where T:UComponent { if (mComponentCount < mMaxComponentCount) { mUComponent [mComponentCount] = mT; mUComponent [mComponentCount].mUEntity = this; if (mWorld != null) { uint tid = mWorld.GetComponentID (mT.GetType ()); mAllBitBunch.SetBit (true, (int)tid); } mComponentCount++; } else { mMaxComponentCount *= 2; UComponent[] tComponent = new UComponent[mMaxComponentCount]; for (int i = 0; i < mComponentCount; i++) tComponent [i] = mUComponent [i]; mUComponent = tComponent; mUComponent [mComponentCount] = mT; mUComponent [mComponentCount].mUEntity = this; if (mWorld != null) { uint tid = mWorld.GetComponentID (mT.GetType ()); mAllBitBunch.SetBit (true, (int)tid); } mComponentCount++; } } public bool DestroyComponent<T>()where T:UComponent { uint tid = mWorld.GetComponentID (typeof(T)); mAllBitBunch.SetBit (false,(int)tid); for (int i = 0; i < mComponentCount; i++) { if (mUComponent [i].GetType () == typeof(T)) { for (int j = i + 1; j < mComponentCount; j++) { mUComponent [j - 1] = mUComponent [j]; } mComponentCount--; return true; } } return false; } public void Release() { for (int i = 0; i < mComponentCount; i++) { mUComponent [i].Release (); } mWorld.deleteEntity (this); } public void Clear() { mComponentCount = 0; mAllBitBunch.SetCount ((int)mWorld.mComponentCount); } public virtual void Init() { } }; public class UComponent { public UEntity mUEntity; public virtual void Release() { } public virtual void Init() { } public virtual UComponent Clone() { return null; } }; public class USystem { public ECSWorld mWorld; public BitBunch mRequestBitBunch = new BitBunch(); public List<UEntity> mListEntity = new List<UEntity> (); public uint power; public void AddRequestComponent(Type mt) { uint tid = mWorld.GetComponentID (mt); mRequestBitBunch.SetBit (true,(int)tid); } public virtual void Update (UEntity uEntity) { } public virtual void Init() { } public static bool operator < (USystem uSystem1,USystem uSystem2) { if (uSystem1.power < uSystem2.power) { return true; } else { return false; } } public static bool operator > (USystem uSystem1,USystem uSystem2) { if (uSystem1.power > uSystem2.power) { return true; } else { return false; } } }; public class ECSWorld { USystem[] mUSystem=new USystem[1024]; UEntity[] mUEntity=new UEntity[1024]; public uint mSystemCount=0; public uint mEntityCount=0; public uint mMaxSystemCount = 1024; public uint mMaxEntityCount = 1024; Dictionary<Type,uint> mDictionary = new Dictionary<Type, uint>(); public uint mComponentCount = 0; public void registerSystem(USystem uSystem) { if (mSystemCount < mMaxSystemCount) { mUSystem [mSystemCount] = uSystem; mUSystem [mSystemCount].mWorld = this; mSystemCount++; } else { mMaxSystemCount *= 2; USystem[] uSystems = new USystem[mMaxSystemCount]; for (int i = 0; i < mSystemCount; i++) uSystems [i] = mUSystem [i]; mUSystem = uSystems; mUSystem [mSystemCount] = uSystem; mUSystem [mSystemCount].mWorld = this; mSystemCount++; } } public void registerEntity(UEntity uEntity) { if (mEntityCount < mMaxEntityCount) { mUEntity [mEntityCount] = uEntity; mUEntity [mEntityCount].mWorld = this; mUEntity [mEntityCount].mWorldID = mEntityCount; mEntityCount++; } else { mMaxEntityCount *= 2; UEntity[] uEntitys = new UEntity[mMaxEntityCount]; for (int i = 0; i < mEntityCount; i++) uEntitys [i] = mUEntity [i]; mUEntity = uEntitys; mUEntity [mEntityCount] = uEntity; mUEntity [mEntityCount].mWorld = this; mUEntity [mEntityCount].mWorldID = mEntityCount; mEntityCount++; } } public void deleteEntity(UEntity uEntity) { for (int i = (int)uEntity.mWorldID; i < mEntityCount-1; i++) { mUEntity [i] = mUEntity [i + 1]; mUEntity [i].mWorldID = (uint)i; } mEntityCount--; } public void registerComponent(Type t) { mDictionary.Add (t,mComponentCount); mComponentCount++; for (int i = 0; i < mSystemCount; i++) mUSystem [i].mRequestBitBunch.SetCount ((int)mComponentCount); for (int i = 0; i < mEntityCount; i++) mUEntity [i].mAllBitBunch.SetCount ((int)mComponentCount); } public uint GetComponentID(Type t) { return mDictionary [t]; } List<UEntity> FindAllEntity(BitBunch mBitBunch) { List<UEntity> mList = new List<UEntity> (); for (int i = 0; i < mEntityCount; i++) { if (BitBunch.Equal(BitBunch.And (mBitBunch, mUEntity [i].mAllBitBunch),mBitBunch)) { mList.Add (mUEntity[i]); } } return mList; } public void Update() { for (int i = 0; i < mSystemCount; i++) { if (mUSystem [i].mListEntity.Count != 0) { for (int j = 0; j < mUSystem[i].mListEntity.Count; j++) { mUSystem [i].Update (mUSystem[i].mListEntity[j]); } continue; } List<UEntity> tList = FindAllEntity (mUSystem[i].mRequestBitBunch); for (int j = 0; j < tList.Count; j++) { mUSystem [i].Update (tList[j]); } } } public virtual void registerAllEntity() { } public virtual void registerAllSystem() { } public virtual void registerAllComponent() { } public void Init() { registerAllSystem (); registerAllEntity (); registerAllComponent (); for (int i = 0; i < mSystemCount; i++) { mUSystem [i].Init (); } ECSSort.QuickSortRelax (mUSystem,0,(int)mSystemCount-1); for (int i = 0; i < mEntityCount; i++) { mUEntity [i].Init (); } } public class ECSSort { public static void QuickSortRelax<T>(IList<T> data)where T:USystem { QuickSortRelax(data, 0, data.Count - 1); } public static void QuickSortRelax<T>(IList<T> data, int low, int high)where T:USystem { if (low >= high) return; T temp = data[(low + high) / 2]; int i = low - 1, j = high + 1; while (true) { while (data[++i] < temp) ; while (data[--j] > temp) ; if (i >= j) break; Swap(data, i, j); } QuickSortRelax(data, j + 1, high); QuickSortRelax(data, low, i - 1); } public static void Swap<T>(IList<T> data,int a,int b) { T temp = data [a]; data [a] = data [b]; data [b] = temp; } } }; };
代码不难写,做两点解释,一,为什么对系统加入了排序机制。二,UEntity和USystem下的BitBunch是干嘛的。
一:
因为我们希望有一些系统是具有执行顺序的,这就像Unity的逻辑脚本可以设置优先级,对系统加入排序机制也是出于同样的考虑,因为一些逻辑系统,如果没有执行顺序的限制,可能会出现错误。
二:
还记得我们说到每一个世界下会把系统所关心的具有特定组件组合的实体选出来吗?这很重要,这个feature完成了对数据、函数的动态的配对。
那么是怎么完成的呢?我们将进行位编码。比如有4个组件,那么组件编号就是0000-1111。
显然只要实体的组件包含系统所需组件即可。也就是1111可以被0011所配对。即做这样一个位运算A&B=?B即可。
其他的一些比如我们用到了一些反射的性质,因为组件的类别最终要映射成一个数字,而字典里所存的就是typeof(T)所得到的Type。