【转载】ECS架构(Entity-Component-System)

仅供学习使用,原文转载 https://zhuanlan.zhihu.com/p/53739106

游戏主循环基本流程

一个经典的游戏主循环一般会包括几个最主要的部分:

1.渲染循环(Rendering Tick)

渲染循环需要做的事情比较多,比如提交GPU需要的数据(顶点属性,相机等),更新动画,更新粒子引擎等等,渲染引擎主要就是负责更新每一帧用户会看到的东西。

2. 物理循环(Physics Tick)

物理循环顾名思义就是负责更新物理逻辑的,比如碰撞检测,力学模拟(包括重力,碰撞产生的作用力这些东西)等等。

3. 逻辑循环(Logic Tick)

逻辑循环就是游戏逻辑的循环,通常游戏引擎都会有分离的逻辑层和渲染层,渲染层靠逻辑层来驱动,比如播放动画,角色移动,跳跃,战斗等等还有游戏玩法相关的东西。

所以一个游戏主循环我们可以简单的用代码描述

void GameWorld::Tick(float DeltaSeconds)
{
    // Logic 
    LogicWorld->Tick(DeltaSeconds);

    // Physics
    PhysicalWorld->Tick(DeltaSeconds);

    // Rendering
    RenderingLayer->Tick(DeltaSeconds);
}

为了让逻辑层和渲染层能以不同的帧率运行(实际上大部分游戏都是这么做的),我们可以把逻辑层单独抽出来用独立的逻辑线程跑,逻辑层和其它层通过消息机制来交互。为了保证物理和渲染的同步性,渲染线程每一帧都需要在帧末尾Sync物理模拟结果

逻辑层的ECS架构

ECS要解决的问题就是传统面向对象框架下GameObject一但过多会造成实例难以维护,耦合过重的问题,所以ECS的核心思想就是把所有的实体都抽象为Entity,所有的数据都抽象为Component,所有的功能描述都抽象为System,通过组合的方式来配置和生成GameObject。

                                              逻辑层、渲染层、物理层的架构

逻辑层的任务前面说了就是计算每一个逻辑帧内,每一个GameObject要做什么,然后GameObject再责计算每个Component要做什么。所以用代码描述就是这样:

// 逻辑层World的Tick
void LogicWorld::Tick(float DeltaSeconds)
{
    for(auto Object : Objects)
    {
        Object->Tick(DeltaSeconds);
    }
}

// Object的Tick
void Object::Tick(DeltaSeconds)
{
    for(auto Component : Components)
    {
        Component->Tick(DeltaSeconds);
    }
}

Component里面会维护所有静态数据以及运行时数据(一般可以Context来维护),对这些数据的操作由System来完成。

例子

假设,我现在控制一个GameObject A放了一个技能,这个技能的按键消息传到逻辑层之后会调用主动技能系统ActiveSkillSystem来处理。

void ActiveSkillSystem::PlaySkill(ECSEntity *Entity, string SkillName)
{
    // 先取这个Entity下的ActiveSkillComponent
    auto SKillComponent = Entity->Get<ActiveSkillComponent>();

    // Context里维护了Skill的运行时信息,这里只需要将状态置为播放就好了
    auto ActiveSkillContext = SkillComponent->GetContext(SkillName);
    ActiveSkillContext->SetSkillState(eSKillState::Playing);
}

然后设置好技能状态之后,逻辑层和渲染层就会去做不同的事情:

渲染层要干什么?

一旦把技能动画设置为了播放状态,渲染层就会为技能播放骨骼蒙皮动画,在每一个渲染帧内去计算蒙皮的最新状态,然后存入VBO提交给GPU去渲染蒙皮。

逻辑层要干什么?

一个技能被释放除了最直观的动画之外,还会有许多效果,比如击中敌方,会给敌方带来伤害或者Buff,那么逻辑层一般会在每一个逻辑帧内计算这个技能的伤害框,如果伤害框在某个逻辑帧内与其他GameObject发生碰撞,那么就会进入后续的伤害计算逻辑,和Buff逻辑,这种情况的时候需要其他的Component来配合比如BuffComponent。

逻辑层如何接入?

基本的逻辑层架构确定之后,要想接入新的游戏逻辑也很方便,首先需要定义好GameObject,然后配置好需要的Component,如果不满足需求的话也可以继承Component基类自己实现一个Component,然后根据需求定义好Systems,最后把GameObject加入到World就可以了。

总结

这篇文章只简单的讲了一下逻辑层应该怎样用ECS的方式来设计,当然ECS的优势还有很多,尤其是在网络同步中,数据和操作的分离使得状态同步逻辑的编写变得十分方便。

posted @ 2021-09-22 21:35  hellogiao1  阅读(1952)  评论(0)    收藏  举报