ECS全称Entity-Component-System,即实体-组件-系统。是一种面向数据(Data-Oriented Programming)的编程架构模式。
这种架构思想是在GDC的一篇演讲《Overwatch Gameplay Architecture and Netcode》(翻成:守望先锋的游戏架构和网络代码)后受到了广泛的学习讨论。在代码设计上有一个原则“组合优于继承”,它的核心设计思想是基于这一思想的“组件式设计”。

某个业务系统筛选出拥有这个业务系统相关组件的实体,对这些实体的相关组件进行处理更新。
Entity数据结构抽象:
| PosiComp | MoveComp | AttrComp | ... |
|---|---|---|---|
| Pos | Velocity | Hp | ... |
| Map | - | Mp | ... |
| - | - | ATK | ... |
个人认为ECS架构的核心是为了解决对象中复杂的聚合问题,能有效的管理代码的复杂度,至于某些场合下的性能的提升,在大多数情况下只是锦上添花的作用(一些SLG游戏具有大量单位可能会有提升吧)。它没有传统OOP编程模式的复杂的继承关系造成的不必要的耦合,结构更加扁平化,相比之下更易于业务的阅读理解和拓展。但这种技术并非是完美无缺的,它十分不擅长单个实体需要连续处理业务(如序列化等)或实体之间相互关联等场合(如更新两个实体的距离),而且对于一些业务逻辑相对固定的模块或者一些底层模块来说,松耦合和管理复杂度可能不是首要问题,有可能在设计上硬拗ECS组件式设计反而带来困扰。对于游戏来说,ECS架构在GamePlay上的实用程度相对较高,在其他符合其特性的模块如(网络模块)也能提供一些不同以往的解题思路。
Q:有些数据只需要一份或被全局访问等情况下,没必要挂载在Entity上和筛选
A:使用单例组件,和其他组件一样是纯数据,但是可以通过单例全局访问,即可以被任意系统任意访问。
Q:有些处理方法,不适合进行批量处理(例如计算两个单位的距离,没必要弄个系统每个单位都相互计算距离)
A:用工具方法,它通常是无副作用的,不会改变任何状态,只返回计算结果
Q: 假设有渲染系统和碰撞系统,要像在这一帧正确的渲染目标的位置,就需要碰撞系统先更新位置信息,渲染系统在进行位置,需要正确处理系统间的前后依赖关系。
A:一个很自然的思路就是分层,根据不同层级的优先级进行处理。由此提出流水线(Piepline)的抽象,定义一颗树和相关节点,系统挂载在其节点上,运行时以某种顺序(先序遍历)展开,同一个节点的系统可以并行(没有依赖)。有需要的话流水线还可以定义系统/实体/组件的初始化等其他问题。
Q:“原教旨主义”的ECS框架有ECS帧的概念,系统会在每一帧重新筛选需要处理的Entity。这种处理方式引起了很大的争论,大家认为是有一些优化空间。
A:社区中几乎没人赞同“原教旨主义”的做法,原因很简单:很多Entity在整个生命周期中都没组件的增删操作,还有相当部分有的有增删操作的Entity其操作频率也很低,每帧都遍历重新筛选代价相对太过昂贵,所以有人提出了缓存、分类、延迟增删操作等思路。一种思路是:Entity的增删/组件的增删的操作进行缓存,延迟到该系统运行时在进行评估筛选,以减少遍历和重复操作。
Q:并不是每个Entity运行期都会改变动态变更组件,有些Entity在运行期压根就不变更组件,甚至它只被编译期就知道的指定System处理。也有些System不在运行期筛选Entity,要么编译期就知道处理哪些Entity,要么是处理一些单例组件。所以有人提出要不要对Entity和System对它们是否在运行期动态操作进行分类,以提升效率。
A:个人认为,Entity不变更组件,本身变动消息就很少只有增删,配合一些缓存、延迟筛选等方法其实没什么影响。不动态筛选Entity的System倒是可以分类型关闭Entity筛选。
Q:ECS是“自驱式”的更新,就像是U3D的Mono的Update方法更新。还有一种响应式的更新,即基于消息事件的通知。“原教旨主义”式的ECS框架是完全自驱的,没有消息机制。系统之间“消息传递”是通过组件的数据传递的,所以在处理“当进入地图时”这种场合,只能使用“HasEnterMap”或者“Enum.EnterMap”之类的标签,或者添加一个“EnterMapComponent"来处理。
A:个人倾向于加入一些消息的处理机制,可以更灵活些。基本思路是:给System添加一个收件箱,收到的消息放在收件箱的队列里。Entity相关变更(增删、变更组件)的一些消息单独使用一个队列管道,在系统刷新的时候首先处理Entity变更消息,进行评估筛选Entity,然后处理信箱里的其他消息,然后在处理System的更新逻辑。
Q:批量处理数据在物理内存连续的场合有利于CPU缓存机制,关键是如何让数据的内存连续。首先想到的是使用数组,那么是组件使用数组还是Entity使用数组呢?
A:如果是组件使用数组,那么当系统处理的Entity包含多个组件的话,那么内存访问会在不同的数组中“跳来跳去”,优化效果十分有限。个人认为若是一定要优化内存访问,关键是保证组件一样的Entity存放在连续内存(Chuck)中,这样保证System访问Entity的内存连续,具体实现方案可以参考U3D的ECS设计Archetype和Chuck。另外,也有对象池的优化空间。上面提到,ECS并不是主要解决性能问题的,只是顺带的,不必太过于执着,当然有也是极好的~。
Unity ECS引入了Archetype和Chuck两个概念,Archetype即为Entity对应的所有组件的一个组合,然后多个Archetypes会打包成一个个Archetype chunk,按照顺序放在内存里,当一个chunck满了,会在接下来的内存上的位置创建一个新的chunk。因此,这样的设计在CPU寻址时就会更容易找到Entity相关的component

using System;
using System.Collections.Generic;
using System.Threading;
namespace ECSDemo
{
public class Singleton<T> where T : Singleton<T>, new()
{
private static T inst;
public static T Inst
{
get
{
if (inst == null)
inst = new T();
return inst;
}
}
}
#region Component 组件
public class Component
{
}
public class SingleComp<T> : Singleton<T> where T : Singleton<T>, new()
{
//
}
#endregion
#region Entity 实体
public class EntityFactory
{
static long eid = 0;
public static Entity Create()
{
Entity e = new Entity(eid);
eid++;
EntityChangedMsg.Inst.Pub(e);
return e;
}
public static Entity CreatePlayer()
{
var e = Create();
e.AddComp(new PosiComp());
e.AddComp(new NameComp() { name = "Major" });
return e;
}
public static Entity CreateMonster(string name)
{
var e = Create();
e.AddComp(new PosiComp());
e.AddComp(new NameComp() { name = name });
return e;
}
}
public class Entity
{
long instID = 0;
public long InstID { get => instID; }
public Entity(long id) { instID = id; }
// 预计一个Entity组件不会很多,故使用链表...
List<Component> comps = new();
public void AddComp<T>(T t) where T : Component
{
comps.Add(t);
EntityChangedMsg.Inst.Pub(this);
}
public void RemoveComp<T>(T t) where T : Component
{
comps.Remove(t);
EntityChangedMsg.Inst.Pub(this);
}
public T GetComp<T>() where T : Component
{
foreach (var comp in comps)
if (comp is T) return comp as T;
return default(T);
}
public bool ContrainComp(Type type)
{
foreach (var comp in comps)
if (comp.GetType() == type) return true;
return false;
}
}
#endregion
#region System 系统
public class System
{
protected SystemMsgBox msgBox = new();
public virtual void Run()
{
msgBox.Each();
OnRun();
}
public virtual void OnRun()
{
}
}
public class SSystem : System
{
//
}
public class DSystem : System
{
protected Dictionary<long, Entity> entities = new();
protected List<Type> conds = new();
HashSet<Entity> evalSet = new();
public DSystem()
{
msgBox.Sub(EntityChangedMsg.Inst, (msg) => {
var body = (EntityChangedMsg.MsgBody)msg;
var e = body.Value;
evalSet.Add(e);
});
}
public void Evalute(Entity e)
{
var id = e.InstID;
bool test = true;
foreach (var cond in conds)
if (!e.ContrainComp(cond))
{
test = false;
break;
}
Entity cache;
entities.TryGetValue(id, out cache);
if (test)
if (cache == null) entities.Add(id, e);
else
if (cache != null) entities.Remove(id);
}
public override void Run()
{
msgBox.EachEntityMsg();
foreach (var e in evalSet)
Evalute(e);
evalSet.Clear();
msgBox.Each();
OnRun();
}
}
#endregion
#region Pipline 流水线
public class Pipeline<ENode, V>
{
public class Node<NENode, NV>
{
List<NV> items = new();
NENode node;
Node<NENode, NV> parent;
List<Node<NENode, NV>> childern = new();
public List<Node<NENode, NV>> Childern { get => childern; }
public List<NV> Items { get => items; }
public Node(NENode n)
{
node = n;
}
public void AddChild(Node<NENode, NV> c)
{
childern.Add(c);
c.parent = this;
}
public void RemoveChild(Node<NENode, NV> c)
{
childern.Remove(c);
c.parent = null;
}
public void AddItem(NV v)
{
items.Add(v);
}
public void RemoveItem(NV v)
{
items.Remove(v);
}
}
Node<ENode, V> root;
Dictionary<ENode, Node<ENode, V>> dict = new();
public Pipeline(ENode node)
{
root = new Node<ENode, V>(node);
dict.Add(node, root);
}
public void AddNode(ENode n)
{
Node<ENode, V> p = root;
AddNode(n, p);
}
public void AddNode(ENode n, Node<ENode, V> p)
{
var node = new Node<ENode, V>(n);
p.AddChild(node);
dict.Add(n, node);
}
public void AddNode(ENode n, ENode p)
{
Node<ENode, V> node;
dict.TryGetValue(p, out node);
if (node != null)
AddNode(n, node);
}
public void AddItem(ENode n, V item)
{
Node<ENode, V> node;
dict.TryGetValue(n, out node);
if (node != null)
node.AddItem(item);
}
public void RemoveItem(ENode n, V item)
{
Node<ENode, V> node;
dict.TryGetValue(n, out node);
if (node != null)
node.RemoveItem(item);
}
protected void Traveral(Action<V> action)
{
TraveralInner(root, action);
}
protected void TraveralInner(Node<ENode, V> node, Action<V> action)
{
var childern = node.Childern;
var items = node.Items;
foreach (var child in childern)
TraveralInner(child, action);
foreach (var item in items)
action(item);
}
}
public class SystemPipeline : Pipeline<ESystemNode, System>
{
public SystemPipeline(ESystemNode en) : base(en)
{
//
}
public void Update()
{
Traveral((sys) => sys.Run());
}
}
public enum ESystemNode : int
{
Root = 0,
Base = 1,
FrameWork = 2,
GamePlay = 3,
}
#endregion
#region World 世界
public class World : Singleton<World>
{
SystemPipeline sysPipe;
public void Init()
{
sysPipe = SystemPipelineTemplate.Create();
}
public void Update()
{
sysPipe.Update();
}
}
#endregion
#region Event 事件
public class Event<T> : Singleton<Event<T>>
{
List<Action<T>> actions = new();
public void Sub(Action<T> action)
{
actions.Add(action);
}
public void UnSub(Action<T> action)
{
actions.Remove(action);
}
public void Pub(T t)
{
foreach (var action in actions)
action(t);
}
}
public class EveEntityChanged : Event<Entity> { }
public interface IMsgBody
{
Type Type();
}
public interface IMsg
{
void Sub(MsgBox listener);
void UnSub(MsgBox listener);
}
public class Msg<T> : Singleton<Msg<T>>, IMsg
{
public class MsgBody : IMsgBody
{
public MsgBody(T v, Type ty) { Value = v; type = ty; }
Type type;
public T Value { private set; get; }
public Type Type()
{
return type;
}
}
List<MsgBox> listeners = new();
public void Sub(MsgBox listener)
{
listeners.Add(listener);
}
public void UnSub(MsgBox listener)
{
listeners.Remove(listener);
}
public void Pub(T t)
{
var msgBody = new MsgBody(t, this.GetType());
foreach (var listener in listeners)
listener.OnMsg(msgBody);
}
}
public class EntityChangedMsg : Msg<Entity> { }
public class MsgBox
{
protected Queue<IMsgBody> msgs = new();
protected Dictionary<Type, Action<IMsgBody>> handles = new();
public virtual void OnMsg(IMsgBody body)
{
msgs.Enqueue(body);
}
public void Sub(IMsg msg, Action<IMsgBody> cb)
{
msg.Sub(this);
handles.Add(msg.GetType(), cb);
}
public void UnSub(IMsg msg, Action<IMsgBody> cb)
{
msg.UnSub(this);
handles.Remove(msg.GetType());
}
public virtual void Each()
{
while (msgs.Count != 0)
{
var msg = msgs.Dequeue();
var type = msg.Type();
Action<IMsgBody> handle;
handles.TryGetValue(type, out handle);
if (handle != null)
handle(msg);
}
}
}
public class SystemMsgBox : MsgBox
{
Queue<IMsgBody> entityMsgs = new();
public override void OnMsg(IMsgBody body)
{
if (body.Type() == typeof(EntityChangedMsg))
entityMsgs.Enqueue(body);
else
msgs.Enqueue(body);
}
public void EachEntityMsg()
{
while (entityMsgs.Count != 0)
{
var msg = entityMsgs.Dequeue();
var type = msg.Type();
Action<IMsgBody> handle;
handles.TryGetValue(type, out handle);
if (handle != null)
handle(msg);
}
}
public override void Each()
{
while (msgs.Count != 0)
{
var msg = msgs.Dequeue();
var type = msg.Type();
Action<IMsgBody> handle;
handles.TryGetValue(type, out handle);
if (handle != null)
handle(msg);
}
}
}
#endregion
#region AppTest
public class AppComp : SingleComp<AppComp>
{
public bool hasInit;
}
public class MapComp : SingleComp<MapComp>
{
public bool hasInit;
public int monsterCnt = 2;
}
public class PosiComp : Component
{
public int x;
public int y;
}
public class NameComp : Component
{
public string name = "";
}
public class AppSystem : SSystem
{
public override void OnRun()
{
if (!AppComp.Inst.hasInit)
{
AppComp.Inst.hasInit = true;
Console.WriteLine("App 启动");
}
}
}
public class SystemPipelineTemplate
{
public static SystemPipeline Create()
{
SystemPipeline pipeline = new(ESystemNode.Root);
// 基本系统
pipeline.AddNode(ESystemNode.Base, ESystemNode.Root);
pipeline.AddItem(ESystemNode.Base, new AppSystem());
pipeline.AddNode(ESystemNode.GamePlay, ESystemNode.Root);
pipeline.AddItem(ESystemNode.GamePlay, new PlayerSystem());
pipeline.AddItem(ESystemNode.GamePlay, new MapSystem());
return pipeline;
}
}
public class MapSystem : DSystem
{
public MapSystem() : base()
{
conds.Add(typeof(PosiComp));
conds.Add(typeof(NameComp));
}
public override void OnRun()
{
if (!MapComp.Inst.hasInit)
{
MapComp.Inst.hasInit = true;
for (int i = 0; i < MapComp.Inst.monsterCnt; i++)
EntityFactory.CreateMonster($"Monster{i + 1}");
Console.WriteLine($"进入地图 生成{MapComp.Inst.monsterCnt}只小怪");
}
foreach (var (id, e) in entities)
{
var name = e.GetComp<NameComp>().name;
var x = e.GetComp<PosiComp>().x;
var y = e.GetComp<PosiComp>().y;
Console.WriteLine($"【{name}】 在地图的 x = {x}, y = {y}");
}
}
}
public class PlayerComp : SingleComp<PlayerComp>
{
public Entity Major;
}
public class PlayerSystem : SSystem
{
public override void OnRun()
{
base.OnRun();
if (PlayerComp.Inst.Major == null)
PlayerComp.Inst.Major = EntityFactory.CreatePlayer();
if (Console.KeyAvailable)
{
int dx = 0;
int dy = 0;
ConsoleKeyInfo key = Console.ReadKey(true);
switch (key.Key)
{
case ConsoleKey.A:
dx = -1;
break;
case ConsoleKey.D:
dx = 1;
break;
case ConsoleKey.W:
dy = 1;
break;
case ConsoleKey.S:
dy = -1;
break;
default:
break;
}
if (dx != 0 || dy != 0)
{
var comp = PlayerComp.Inst.Major.GetComp<PosiComp>();
if (comp != null)
{
Console.WriteLine($"玩家移动 Delta X = {dx}, Delta Y = {dy}");
comp.x += dx;
comp.y += dy;
}
}
}
}
}
#endregion
class Program
{
static void Main(string[] args)
{
World.Inst.Init();
while (true)
Loop();
}
public static void Loop()
{
World.Inst.Update();
Console.WriteLine("--------------------------------------------");
Thread.Sleep(1000);
}
}
}
[AttributeUsage(AttributeTargets.Class)]
public class SystemPipelineAttr : Attribute
{
public ESystemNode Type;
public SystemPipelineAttr(Type type = null)
{
this.Type = type;
}
}
[SystemPipelineAttr(ESystemNode.GamePlay)]
public class MapSystem {} // ...
// ...
public static Dictionary<string, Type> GetAssemblyTypes(params Assembly[] args)
{
Dictionary<string, Type> types = new Dictionary<string, Type>();
foreach (Assembly ass in args)
{
foreach (Type type in ass.GetTypes())
{
types[type.FullName] = type;
}
}
return types;
}
// ...
foreach (Type type in types[typeof (SystemPipelineAttr)])
{
object[] attrs = type.GetCustomAttributes(typeof(SystemPipelineAttr), false);
foreach (object attr in attrs)
{
SystemPipelineAttr attribute = attr as SystemPipelineAttr;
// ...
}
}
我是一名决定学习Ruby和RubyonRails的ASP.NETMVC开发人员。我已经有所了解并在RoR上创建了一个网站。在ASP.NETMVC上开发,我一直使用三层架构:数据层、业务层和UI(或表示)层。尝试在RubyonRails应用程序中使用这种方法,我发现没有关于它的信息(或者也许我只是找不到它?)。也许有人可以建议我如何在RubyonRails上创建或使用三层架构?附言我使用ruby1.9.3和RubyonRails3.2.3。 最佳答案 我建议在制作RoR应用程序时遵循RubyonRails(RoR)风格。Rails
目录0专栏介绍1平面2R机器人概述2运动学建模2.1正运动学模型2.2逆运动学模型2.3机器人运动学仿真3动力学建模3.1计算动能3.2势能计算与动力学方程3.3动力学仿真0专栏介绍?附C++/Python/Matlab全套代码?课程设计、毕业设计、创新竞赛必备!详细介绍全局规划(图搜索、采样法、智能算法等);局部规划(DWA、APF等);曲线优化(贝塞尔曲线、B样条曲线等)。?详情:图解自动驾驶中的运动规划(MotionPlanning),附几十种规划算法1平面2R机器人概述如图1所示为本文的研究本体——平面2R机器人。对参数进行如下定义:机器人广义坐标
网站的日志分析,是seo优化不可忽视的一门功课,但网站越大,每天产生的日志就越大,大站一天都可以产生几个G的网站日志,如果光靠肉眼去分析,那可能看到猴年马月都看不完,因此借助网站日志分析工具去分析网站日志,那将会使网站日志分析工作变得更简单。下面推荐两款网站日志分析软件。第一款:逆火网站日志分析器逆火网站日志分析器是一款功能全面的网站服务器日志分析软件。通过分析网站的日志文件,不仅能够精准的知道网站的访问量、网站的访问来源,网站的广告点击,访客的地区统计,搜索引擎关键字查询等,还能够一次性分析多个网站的日志文件,让你轻松管理网站。逆火网站日志分析器下载地址:https://pan.baidu.
一、机器人介绍 此处是基于MATLABRVC工具箱,对ABB-IRB-1200型号的微型机械臂进行正逆向运动学分析,并利Simulink工具实现对机械臂进行具有动力学参数的末端轨迹规划仿真,最后根据机械模型设计Simulink-Adams联合仿真。 图1.ABBIRB 1200尺寸参数示意图ABBIRB 1200提供的两种型号广泛适用于各作业,且两者间零部件通用,两种型号的工作范围分别为700 mm 和 900 mm,大有效负载分别为 7 kg 和5 kg。 IRB 1200 能够在狭小空间内能发挥其工作范围与性能优势,具有全新的设计、小型化的体积、高效的性能、易于集成、便捷的接
目录一.大致如下常见问题:(1)找不到程序所依赖的Qt库version`Qt_5'notfound(requiredby(2)CouldnotLoadtheQtplatformplugin"xcb"in""eventhoughitwasfound(3)打包到在不同的linux系统下,或者打包到高版本的相同系统下,运行程序时,直接提示段错误即segmentationfault,或者Illegalinstruction(coredumped)非法指令(4)ldd应用程序或者库,查看运行所依赖的库时,直接报段错误二.问题逐个分析,得出解决方法:(1)找不到程序所依赖的Qt库version`Qt_5'
我想使用ruby-prof和JMeter分析Rails应用程序。我对分析特定Controller/操作/或模型方法的建议方法不感兴趣,我想分析完整堆栈,从上到下。所以我运行这样的东西:RAILS_ENV=productionruby-prof-fprof.outscript/server>/dev/null然后我在上面运行我的JMeter测试计划。然而,问题是使用CTRL+C或SIGKILL中断它也会在ruby-prof可以写入任何输出之前杀死它。如何在不中断ruby-prof的情况下停止mongrel服务器? 最佳答案
我尝试用Ruby设计一个基于Web的应用程序。我开发了一个简单的核心应用程序,在没有框架和数据库的情况下在六边形架构中实现DCI范例。核心六边形中有小六边形和网络,数据库,日志等适配器。每个六边形都在没有数据库和框架的情况下自行运行。在这种方法中,我如何提供与数据库模型和实体类的关系作为独立于数据库的关系。我想在将来将框架从Rails更改为Sinatra或数据库。事实上,我如何在这个核心Hexagon中实现完全隔离的rails和mongodb的数据库适配器或框架适配器。有什么想法吗? 最佳答案 ROM呢?(Ruby对象映射器)。还有
文章目录认识unity打包目录结构游戏逆向流程Unity游戏攻击面可被攻击原因mono的打包建议方案锁血飞天无限金币攻击力翻倍以上统称内存挂透视自瞄压枪瞬移内购破解Unity游戏防御开发时注意数据安全接入第三方反作弊系统外挂检测思路狠人自爆实战查看目录结构用il2cppdumper例子2-森林whoishe后记认识unity打包目录结构dll一般很大,因为里面是所有的游戏功能编译成的二进制码游戏逆向流程开发人员代码被编译打包到GameAssembly.dll中使用il2ppDumper工具,并借助游戏名_Data\il2cpp_data\Metadata\global-metadata.dat
在笔者前面有一篇文章《驱动开发:断链隐藏驱动程序自身》通过摘除驱动的链表实现了断链隐藏自身的目的,但此方法恢复时会触发PG会蓝屏,偶然间在网上找到了一个作者介绍的一种方法,觉得有必要详细分析一下他是如何实现的进程隐藏的,总体来说作者的思路是最终寻找到MiProcessLoaderEntry的入口地址,该函数的作用是将驱动信息加入链表和移除链表,运用这个函数即可动态处理驱动的添加和移除问题。MiProcessLoaderEntry(pDriverObject->DriverSection,1)添加MiProcessLoaderEntry(pDriverObject->DriverSection,
“架设一个亿级高并发系统,是多数程序员、架构师的工作目标。许多的技术从业人员甚至有时会降薪去寻找这样的机会。但并不是所有人都有机会主导,甚至参与这样一个系统。今天我们用12306火车票购票这样一个业务场景来做DDD领域建模。”开篇要实现软件设计、软件开发在一个统一的思想、统一的节奏下进行,就应该有一个轻量级的框架对开发过程与代码编写做一定的约束。虽然DDD是一个软件开发的方法,而不是具体的技术或框架,但拥有一个轻量级的框架仍然是必要的,为了开发一个支持DDD的框架,首先需要理解DDD的基本概念和核心的组件。一.什么是领域驱动设计(DDD)首先要知道DDD是一种开发理念,核心是维护一个反应领域概