当前位置: 首页 > news >正文

战斗系统架构:为什么游戏战斗适合ECS架构?

"游戏战斗"是游戏项目中的核心设计。最近10年,基本上我们游戏项目的战斗系统,无论是服务端与客户端,都是基于ECS模式来进行设计。(补充说明: 这里的ECS,不特指Unity (DOTS)或某个引擎的特指工具集,而是一种架构与设计)。游戏战斗是最符合ECS的设计思想与模式的,在一个世界里面来对所有的战斗单元进行迭代计算。

对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀

1: 主流游戏战斗系统的需求分析

1: 游戏世界,负责游戏的逻辑管理,游戏的进程管理,负责游戏战斗单元的管理;

2: 游戏世界中的角色:玩家(主角,其它玩家角色,小兵队友等),敌人(近战小怪,远战小怪,精英怪,Boss怪等),道具(戒子,宝石,加速鞋等), 传送门,NPC(任务NPC, 商城兑换NPC等),游戏子弹等。

3: 游戏中的技能与Buff,通过技能来驱动动作动画+伤害计算。通过Buff来改变计算时候的一些属性,能让不同装备,道具等情况下的角色获得不一样的数值体验。

4: 游戏的目标与决策,挑战中敌人如何决策能给玩家造成困难,玩家操作领取特定的任务,完成特定的目标。

5: 游戏核心的战斗数值表:角色数值表,技能数值表,Buff数值,道具数值表等。

以上就是游戏中的主流战斗中的核心需求与核心数据。游戏战斗的本质,移动行走,播放动画+套数值计算公式,过程中获得道具or奖励加Buff来提升数值能力。最后完成目标or任务,一阶段结束。

2: ECS设计模式的概述

概念1: Entity,游戏世界中的角色实体,是由多个不同功能的数据组件组成。(不同于游戏引擎中的GameObject or Node)。 

概念2: Component, 组成Entity的数据组件,这里没有任何逻辑,只有特定功能需要用到的数据。

概念3:System,游戏世界中完成特定功能的核心迭代计算。每个不同功能都是一个独立的System迭代计算。

概念4: World: 游戏世界,管理所有的Entity, World的Update中迭代所有的战斗逻辑System的迭代计算。管理好游戏进程,游戏暂停,游戏Entity。World基于某场战斗的一个单例,及每场游戏战斗有只有一个World。

3: 如何基于ECS来设计核心战斗

基于ECS战斗的核心流程:

步骤1: 创建游戏战斗世界对象,用来管理本场战斗;

步骤2: 根据地图 or网络消息,来创建游戏战斗单元与角色;

步骤3:创建游戏玩家控制角色;

步骤4:开启游戏世界的逻辑迭代;

每个战斗单元的Entity设计,每个组件为特定功能迭代需要的数据,伪代码如下:

export class BaseEntity {    public worldId: number = -1; // 角色在世界里面的Id;    // 各个与战斗相关的组件    public uInfo: CharactorInfoComponent; // 角色的相关信息    public uTransform: TransformComponent; // 位置旋转    public uStatus: StatusComponent; // 玩家的状态数据相关;    public uNav: NavComponent; // 玩家导航中需要保存的数据;    public uSkillAndBuff: SkillAndBuffComponent; // 玩家释放技能与Buff相关数据;    public uRvo: RVOComponent;    public uAStarNav: AStarComponent; // 玩家A*寻路的时候需要保存的导航的数据;    public uProps: PropsComponent; // 玩家单机游戏需要的战斗+移动数据,帧同步+单机需要;}
export class CharactorEntity2D extends BaseEntity {    public uAnim: Anim2DComponent; // cocos节点or动画相关;    public uThink: ThinkComponent; // 角色做决策思考的组件}

worldId: 唯一表示的角色的ID;

uInfo: 存放游戏角色相关的信息,包括基础数值等;

uTransform: 角色在地图中的位置+朝向;

uStatus: 角色的状态信息;

uSkillAndBuff: 角色技能与Buff的核心数据;

uRvo: 角色做动态壁障需要用到的数据;

uAStarNav: 角色基于AStar地图的寻路与导航所需核心数据;

uProps: 角色在战斗中的一些常用的属性数据值;

uAnim: 与游戏引擎相关显示节点的数据组件,包括GameObject,引擎动画;

...

export class CharactorInfoComponent {    public unick: string;    public job: number;    public sex: number;    public charactorId: number;    public typeId: number; // 增加一个类型id,不用每次都去计算;    public group: number = -1; // 角色属于哪个分组    public charactorData: any = null; // 增加一个角色得类型数据,减少string转int这些;}
export enum EntityType {    Hero = 1, // 英雄    Legion = 2, // 军团    MonsterAndTower = 3, // 怪物与塔    Tower = 4, // 塔    Building = 5, //建筑    Transfer = 6, // 传送门

角色信息组件,包含了数值表中的数据,CharactorData, typeId,group分组,技能与Buff,显示节点等。其中通过CharactorData,typeId来区分不同角色的数值,通过uAnim组件区分角色的外观与显示,由不同的数值(CharactorData)+类型id+技能与Buff(uSkillAndBuff)+动画(uAnim)组合成不同角色。以此来代替传统复杂的继承体系。

在战斗世界World中,对所有的角色Entity进行管理,提供一些访问遍历这些角色Entity的接口函数,方便对战场的整体形式做好数据采集。比如周围的敌人收集等常规战斗算法与计算。方便为特定的System逻辑迭代提供访问战场特定的Entity的数据接口。

接口1: 遍历给定的某种charactorId类型的角色,如遍历所有的"影子刺客"

    public ForEachCharactorEntity(charactorId: number, func): void {                for (let worldId in this.charactors) {            let e: CharactorEntity2D = this.charactors[worldId];            if(EntityCtrlUtils.IsEntityInDied(e)) {                continue;            }            // console.log(e, e.uInfo.typeId, entityType);            if(e.uInfo.charactorId ===  charactorId) {                let ret = func(e);                if(ret < 0) { // 返回-1,停止遍历                    break;                }            }        }    }

接口2: 遍历某种类型的所有角色,如遍历敌人。

    public ForEachTypeEntity(entityType: number, func): void {                for (let worldId in this.charactors) {            let e: CharactorEntity2D = this.charactors[worldId];            if(EntityCtrlUtils.IsEntityInDied(e) ) {                continue;            }            // console.log(e, e.uInfo.typeId, entityType);            if(e.uInfo.typeId ===  entityType) {                let ret = func(e);                if(ret < 0) { // 返回-1,停止遍历                    break;                }            }        }    }

接口3: 遍历某个小团体的所有的角色,如遍历A组的所有怪物:

    public ForEachGroupEntity(group: number, func): void {        if(group === -1) { // 单独的个体分组,直接返回            return;        }        for (let worldId in this.charactors) {            let e: CharactorEntity2D = this.charactors[worldId];            if(EntityCtrlUtils.IsEntityInDied(e) ) {                continue;            }            // console.log(e, e.uInfo.typeId, entityType);            if(e.uInfo.group ===  group) {                let ret = func(e);                if(ret < 0) { // 返回-1,停止遍历                    break;                }            }        }    }

......

在战斗世界World的Update种,迭代计算每个逻辑

// 本地的迭代    protected WorldUpdate(dt: number): void {        if(this.state !== FightState.Playing) { // 游戏暂停中,不做迭代计算            return;        }        // 处理上一帧的碰撞引擎的碰撞对        BulletCalcSystem.Instance.Update(dt);        // 军团策略决策        LegionCtrlSystem.Instance.Update(dt);        // 怪物策略策略        MonsterCtrlSystem.Instance.Update(dt);        // 本地导航的迭代;        this.NavSystemUpdate(dt);        // 技能与Buff迭代        this.OnSelectSkillUpdate(dt);        this.OnSkillAndBuffUpdate(dt);        // 子弹迭代        this.OnBulletsUpdate(dt);        // 同类物体的互斥控制        EntityMutexSystem.Instance.update(dt);        // 复活需要复活的军团        this.OnReviveLegionUpdate(dt);        // 角色动画同步迭代        this.CharactorAnimStatusUpdate();        // 角色血条同步        UpdateBloodSystem.update(dt);        // 子弹,角色删除回收        this.OnDeleteEntitiesUpdate();        // end        // 传送门传送Hero        if(this.isShowTransfer) {            this.OnTransferHeroUpdate();        }        // end    }

每个功能的迭代计算,由单独的System来进行迭代和处理,相对独立,互不干扰。每新增一个功能,就增加一个相对功能的System迭代。

以怪物思考的System迭代为例子:

/**      * @zh 粗略计算怪物可以的操作。     * @example    */    public MonsterRoughMarkTaskMask() {        // var player: CharactorEntity2D = FightWorldMgr.Instance.GetPlayer();        // 标记军团每个成员,让它们根据优先级做决策;        FightWorldMgr.Instance.ForEachTypeEntity(EntityType.MonsterAndTower, (entity: CharactorEntity2D)=>{            if(entity.uStatus.status === CharactorStatus.Vertigo) { // 有晕眩Buff, 什么也不干                return 1;            }            entity.uThink.taskStateMask = EntityTaskState.BodyIdle;            if(entity.uInfo.charactorData.Range <= 0) { // 没有任何攻击属性的建筑;                return 1;            }            // 做一些决策的前置处理            this.BeforeRoughMarkForEntity(entity);            // console.log(entity.uThink.ActiveAttackEntityId);            var warFiledR = this.GetWarFiledR(entity);            var dis = Vec3.squaredDistance(entity.uTransform.pos, this.GetWarFiledCenterPos(entity));            if((entity.uThink.prevTask === EntityTaskState.GotoSpawnPoint && entity.uStatus.status === CharactorStatus.Run) ||                 dis > warFiledR * warFiledR) {                entity.uSkillAndBuff.comboCount = 0;                entity.uSkillAndBuff.comboSkillOptNode = null;                entity.uThink.ActiveAttackEntityId = -1; // 已经有敌人了,范围内的,清理仇恨列表                entity.uThink.taskStateMask |= EntityTaskState.GotoSpawnPoint;                this.EndRoughMarkForEntity(entity, EntityTaskState.GotoSpawnPoint);                return 1;            }            else if(entity.uSkillAndBuff.comboCount > 0) { // 连击没有发完,直接发                entity.uThink.taskStateMask |= EntityTaskState.AttackTarget;                entity.uThink.taskStateMask |= EntityTaskState.HasEnemySituation; // 有敌情;                entity.uThink.ActiveAttackEntityId = -1; // 已经有敌人了,范围内的,清理仇恨列表                this.EndRoughMarkForEntity(entity, EntityTaskState.AttackTarget);            }            else if(EntityCtrlUtils.IsEntityInAttacking(entity)) {                 entity.uThink.taskStateMask |= EntityTaskState.AttackTarget; // 攻击范围,嘲讽Buff, 警戒范围,仇恨列表                entity.uThink.taskStateMask |= EntityTaskState.HasEnemySituation; // 有敌情;                entity.uThink.ActiveAttackEntityId = -1; // 已经有敌人了,范围内的,清理仇恨列表                this.EndRoughMarkForEntity(entity, EntityTaskState.AttackTarget);                return 1;            }            else if(EntityCtrlUtils.HasEnemyInEntityVisionWithCached(entity, [EntityType.Hero, EntityType.Legion])) {                entity.uThink.taskStateMask |= EntityTaskState.AttackTarget; // 攻击范围,嘲讽Buff, 警戒范围,仇恨列表                entity.uThink.taskStateMask |= EntityTaskState.HasEnemySituation; // 有敌情;                entity.uThink.ActiveAttackEntityId = -1; // 已经有敌人了,范围内的,清理仇恨列表                this.EndRoughMarkForEntity(entity, EntityTaskState.AttackTarget);                return 1;            }            else if(entity.uThink.ActiveAttackEntityId !== -1) { // 最后才是单独的处理仇恨列表                var target = FightWorldMgr.Instance.GetEntityById(entity.uThink.ActiveAttackEntityId);                if(target && !EntityCtrlUtils.IsEntityInDied(target)) {                    // entity.uThink.taskStateMask |= EntityTaskState.AttackFightMeEntity;                    // this.EndRoughMarkForEntity(entity, EntityTaskState.AttackFightMeEntity);                    // console.log("yes it is AttackFightMeEntity");                    entity.uThink.taskStateMask |= EntityTaskState.AttackTarget                    entity.uThink.taskStateMask |= EntityTaskState.HasEnemySituation; // 有敌情;                    this.EndRoughMarkForEntity(entity, EntityTaskState.AttackTarget);                    return 1;                }                entity.uThink.ActiveAttackEntityId = -1;            }            /*else if(EntityCtrlUtils.HasMateInEntityVisionWithCached(entity, [EntityType.MonsterAndTower])) {                entity.uThink.taskStateMask |= EntityTaskState.HelpMate;                this.EndRoughMarkForEntity(entity, EntityTaskState.HelpMate);                return 1;            }*/            return 1;                    });// end    }
    /**      * @zh 执行军团成员的每个操作。     * @example    */    public MonsterDoActionUpdate(dt: number): void {        FightWorldMgr.Instance.ForEachTypeEntity(EntityType.MonsterAndTower, (entity: CharactorEntity2D)=>{            this.DoMonsterEntityHightPriorityAction(entity, dt);        });// end        FightWorldMgr.Instance.ForEachTypeEntity(EntityType.MonsterAndTower, (entity: CharactorEntity2D)=>{            this.DoMonsterEntityLowPriorityAction(entity, dt);        });// end    }        public Update(dt: number) {        // 粗略标记敌人可以做的任务决策        MonsterCtrlSystem.Instance.MonsterRoughMarkTaskMask();        // end        // 分析处理是否要帮助队友        MonsterCtrlSystem.Instance.MonsterRoughMarkHelpMeta();        // end        // 执行决策        MonsterCtrlSystem.Instance.MonsterDoActionUpdate(dt);        // end    }

这样就通过System,针对Entity来进行迭代与处理,完成对应的迭代处理逻辑。

4: 基于ECS来设计战斗的优势

优点1: 天然的战斗性能定位分析优势:你可以非常方便的注释掉相关的迭代,用来统计与定位哪些迭代会比较消耗性能与计算,从而更精准的优化;

优点2: 针对战斗种的暂停与恢复,可以非常方便的做到。在World中管理暂停即可;

优势3: 可以从全局的视角来进行计算迭代,而不是从某个角色的视角;

优势4: 组织结构更加的清晰规范,基于不同的迭代,分开代码文件,避免一个代码文件中,N个不相关的代码逻辑,也方便重构与替换。

...

END

今晚20:30直播《肉鸽类商业游戏项目如何落地》,里面有一个环节会向大家展示我们基于ECS来做的核心战斗。欢迎大家来直播间交流。

,时长03:12

本周五(2025年10月17日,20:30分)开始,Blake老师全面直播,分析我们《肉鸽原型项目+方案+教程配套》是如何落地的。全程干货。

直播内容专题如下(每个小专题设置Q&A环节):

1: 立项: 为什么我们会选《肉鸽类》的游戏? Q&A

2: 初步策划案我们是如何来落地的? Q&A

3: 面对策划案的需求,如何做的技术选型与技术架构? Q&A

4: 如何解决原型项目的美术资源与为什么先从原型项目开始?Q&A

5: 如何把别人项目的资源,迁移重构到自己的项目框架中? Q&A

6: 策划使用的地图编辑器我们是如何解决的?Q&A

7: 为什么核心战斗逻辑我们采用ECS架构来实现,有什么好处?Q&A

8: 面对5种英雄,8种军团兵种,25种敌人怪物,37种道具,49种技能,32种局内升级,15级的局外天赋升级,我们是如何做好技能与Buff的架构?Q&A

9:面对秒杀,暴击,复活,无敌,局外局内技能等对数值的影响,我们如何做伤害数值计算。Q&A

10:英雄操控军团的策略,军团与敌人的群体战斗,我们是如何设计与实现的?有限状态机,行为决策树?还是其它方案。Q&A

11: 为什么我们做原型的同时要配套教程?Q&A

12: 美术外包,我们是如何梳理自己的需求的?UI, 角色,场景,特效,ICON等。Q&A

本次主题内容众多,我们将竭尽全力,讲好每一个专题,技术分享毫无保留。我们会根据具体的直播进度,将直播分成连续的若干场,本周五20:30开始,抖音直播间不见不散。

http://www.dtcms.com/a/495465.html

相关文章:

  • 【C语言加油站】C语言文件操作完全指南:feof、ferror与缓冲区机制详解
  • 做seo怎么设计网站响应式网站软件
  • 怎么样建网站卖东西可以入侵的网站
  • 17、Centos9 安装 1Panel
  • Linux学习笔记--GPIO控制器驱动
  • 重庆制作手机网站如何看一个站点是不是有wordpress
  • 网站如何在推广设计公司logo软件
  • 价值1w的数据分析课知识点汇总-excel使用(第一篇)
  • android取消每次u盘插入创建无用(媒体)文件夹
  • 个人如何办网站1m的带宽做网站可以吗
  • 多机部署,负载均衡
  • python通过win32com库调用UDE工具来做开发调试实现自动化源码,以及UDE的知识点介绍
  • 关于Unity中ComputeShader 线程id的理解
  • 幽冥大陆(十六)纸币器BV20识别纸币——东方仙盟筑基期
  • 设计网站的方法做彩票网站需要什么条件
  • Windows 平台 HOOK DWM 桌面管理程序,实现输出变形的桌面图像到显示器
  • Java 大视界 -- Java 大数据在智能电网电力市场交易数据分析与策略制定中的关键作用
  • 安徽和县住房城乡建设局网站wordpress移动端顶部导航栏
  • oracle:判断字段不以开头
  • 学习笔记3-深度学习之logistic回归向量化
  • 哈尔滨网站建设网站优秀个人网站设计欣赏
  • 高辐射环境下AS32S601ZIT2型MCU的抗辐照性能与应用潜力分析
  • 基于STM32F407与FT245R芯片实现USB转并口通信时序控制
  • Retrofit 与 OkHttp 全面解析与实战使用(含封装示例)
  • qiankun知识点
  • 面向接口编程与自上而下的系统设计艺术
  • 数据结构基石:单链表的全面实现、操作详解与顺序表对比
  • 网站 无限下拉做一个小程序需要多少钱
  • 【Kubernetes】常见面试题汇总(二十六)
  • 微网站设计制作wordpress在线文档