ECS战斗拆分思路(一)角色

本文探讨了如何使用ECS架构来设计一个清晰且模块化的战斗系统。通过将角色分解为实体、组件和系统,实现了良好的逻辑与数据分离。文中详细介绍了各个组件的功能及其在不同系统中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


layout: post
title: 第一次分享-ECS战斗拆分思路(一)角色
date: 2022-01-24
categories: blog
tags: [技术分享]
description: 分享一下自己的想法,顺便与大家一同交流

已知ECS思想,

把任何事物,拆分成Entity System Component

需要先思考一个角色有哪些基本要素

  1. 它有一个基本的数据来描述它的存在,它是人,还是怪物,还是NPC之类
  2. 它有一些常见的角色属性,HP,Mp,Attack,Dfen,在角色进行战斗时,获取角色的数据,进行公式计算
  3. 它能显示在屏幕里,有一个VO,管理着它的显示,隐藏。。。
  4. 它有基本的位置描述信息,描述他在世界的哪个位置
  5. 技能,AI,Buff,逻辑。。。等等

根据具体功能,拆分出对应的System

比如

  1. 移动管理
  2. 技能释放管理
  3. AI管理
  4. 伤害管理
  5. 。。。等等

于是代码大概这样写

创建一个Entity

然后为Entity添加LifeCom,AttrCom,VoCom,PosCom

移动System 关注 Entity的 VoCom和PosCom
技能System 关注 Entity的 AttrCom,SkillCom,HurtCom
AI System 关注 Entity的 AICom,AttrCom,PosCom,SkillCom

每个系统只关心关注的Component,其他一概不管,各司其职

就做到了逻辑和数据分离

欢迎各位来我的个人博客观光,希望从今天起,持续输出一些自己脑子里有的东西,也希望各位不吝指教

啊安的个人博客

、概述 游戏是个以探索解锁新地区,采集资源以进行地区建设来防范定时对地区发动攻击的动作战斗游戏,核心流程如下: 暂时无法在飞书文档外展示此内容 二、具体功能实现方案 注:代码为临时编写用于示例实现方式,主要为设计思路分享 、动作战斗系统实现 1. 设计相关 角色的移动跳跃可以通过普通的角色控制器实现,不做过多赘述。对于战斗系统,原则上希望原理相对简单,实际的上手需要定门槛,方案为轻攻击,重攻击,跳跃和躲避的排列组合以此派生,核心在于使用不同武器时有不同的连段和不同的躲避性能,以此在游戏内通过随机掉落不同的武器既增添了战斗系统的丰富度同时也限制了通过掉落物获得爆发性的提升可能。 2. 实现相关 战斗系统的设计包含几个要点: - 相对精确的碰撞检测 - 反馈状态系统(包含受击效果、攻击效果等) - 连击的派生与取消 - 帧的动画状态控制 1. 对于碰撞检测,可以考虑使用使用ECS系统单纯实现,如: //攻击实体 public struct HitBox : IComponentData { public float3 Center; public float Radius; public Entity Owner; public int DamageValue; } //受击实体 public struct HurtBox : IComponentData { public float3 Center; public float Radius; public Entity Owner; } // 碰撞检测系统 public class HitDetectionSystem : SystemBase { protected override void OnUpdate() { Entities.WithAll<HitBox>().ForEach((Entity hitEntity, ref HitBox hitBox) => { Entities.WithAll<HurtBox>().ForEach((Entity hurtEntity, ref HurtBox hurtBox) => { if (hitBox.Owner != hurtBox.Owner && math.distance(hitBox.Center, hurtBox.Center) < hitBox.Radius + hurtBox.Radius) { var damageEvent = new DamageEvent { Attacker = hitBox.Owner, Target = hurtBox.Owner, Damage = hitBox.DamageValue }; EntityManager.AddComponentData(hurtEntity, damageEvent); } }).Run(); }).Run(); } } 以上方式可以实现,但是复杂场景下可能会有性能问题,可以使用Job System进行性能优化,如: public struct HitDetectionJob : IJobParallelFor { public NativeArray<HitBoxData> hitBoxes; public NativeArray<HurtBoxData> hurtBoxes; public NativeQueue<DamageEvent>.ParallelWriter damageEvents; //碰撞检测 public void Execute(int index) { var hitBox = hitBoxes[index]; foreach (var hurtBox in hurtBoxes) { if (hitBox.owner != hurtBox.owner && math.distance(hitBox.position, hurtBox.position) < hitBox.radius + hurtBox.radius) { damageEvents.Enqueue(new DamageEvent { attacker = hitBox.owner, target = hurtBox.owner, damage = hitBox.damage }); } } } } 2. 受击反馈系统可以以如下方式实现,如: private Animator animator; private Rigidbody rigidbody; private Coroutine currentReaction; public float hitStunDuration = 0.3f; public float launchForce = 5f; public AnimationCurve hitSlowCurve; public void ProcessHit(HitDirection direction, HitIntensity intensity) { if (currentReaction != null) { StopCoroutine(currentReaction); } currentReaction = StartCoroutine(HitReactionRoutine(direction, intensity)); } private IEnumerator HitReactionRoutine(HitDirection dir, HitIntensity intensity) { //触发受击相关动画 animator.SetTrigger(GetHitTriggerName(dir, intensity)); // 受击相关物理反馈 Vector3 force = GetForceDirection(dir) * intensity.Power; rigidbody.AddForce(force, ForceMode.VelocityChange); // 受击后时间变化 Time.timeScale = 0.2f; yield return new WaitForSecondsRealtime(0.1f); float timer = 0f; while (timer < hitStunDuration) { Time.timeScale = Mathf.Lerp(0.2f, 1f, hitSlowCurve.Evaluate(timer / hitStunDuration)); timer += Time.unscaledDeltaTime; yield return null; } Time.timeScale = 1f; } private string GetHitTriggerName(HitDirection dir, HitIntensity intensity) { return $"Hit_{dir.ToString()}_{intensity.ToString()}"; } 同时受击可以配合相对动态的物理材质控制提升效果,如: private Dictionary<Collider, PhysicMaterial> originalMaterials = new Dictionary<Collider, PhysicMaterial>(); public void ApplyTemporaryMaterial(Collider collider, PhysicMaterial material, float duration) { StartCoroutine(MaterialRoutine(collider, material, duration)); } private IEnumerator MaterialRoutine(Collider collider, PhysicMaterial material, float duration) { if (!originalMaterials.ContainsKey(collider)) { originalMaterials[collider] = collider.material; } collider.material = material; yield return new WaitForSeconds(duration); collider.material = originalMaterials[collider]; } 3. 连击系统的实现,认为是通过状态机控制相对方便,如 private int currentComboStep; private float comboResetTimer; private bool canAcceptNextInput; public class ComboSequence { public string AnimationState; public float InputWindowStart; public float InputWindowDuration; public AttackType[] CancelableAttacks; } public ComboSequence[] comboSequence; void Update() { if (currentComboStep > 0) { comboResetTimer -= Time.deltaTime; if (comboResetTimer <= 0) { ResetCombo(); } } } public void TryNextAttack(AttackType inputType) { if (!canAcceptNextInput) return; var currentStep = comboSequence[currentComboStep]; if (Array.Exists(currentStep.CancelableAttacks, t => t == inputType)) { ExecuteAttack(inputType); } 可以简单实现招式的派生和组合,只是单纯的连段不会有好的操作体验,要有相对克制的预输入才能在游玩体验上更流畅和有章法,如: private Queue<BufferedInput> _inputQueue = new Queue<BufferedInput>(); private float _bufferWindow = 0.15f; void Update() { ProcessExpiredInputs(); } public void BufferInput(InputType type) { _inputQueue.Enqueue(new BufferedInput { type = type, expireTime = Time.time + _bufferWindow }); } //相对克制预输入队列,不将所有的预输入内容都输出 public bool TryConsumeInput(InputType type) { foreach (var input in _inputQueue) { if (input.type == type && Time.time <= input.expireTime) { _inputQueue.Clear(); return true; } } return false; } private void ProcessExpiredInputs() { while (_inputQueue.Count > 0 && _inputQueue.Peek().expireTime < Time.time) { _inputQueue.Dequeue(); } } 4. 使用动画事件来驱动战斗,以实现对战斗节奏的控制,如: private Dictionary<string, AnimationEvent[]> _cachedEvents = new Dictionary<string, AnimationEvent[]>(); public void PrecacheAnimationEvents(Animator animator) { foreach (var clip in animator.runtimeAnimatorController.animationClips) { _cachedEvents[clip.name] = clip.events; clip.events = OptimizeEvents(clip.events); } } private AnimationEvent[] OptimizeEvents(AnimationEvent[] original) { List<AnimationEvent> optimized = new List<AnimationEvent>(); foreach (var evt in original) { if (evt.functionName.StartsWith("Combat/")) { optimized.Add(evt); } } return optimized.ToArray(); } //通过动画事件驱动战斗分发 public void DispatchAnimationEvent(string eventPath) { var parts = eventPath.Split('/'); string systemName = parts[1]; string methodName = parts[2]; switch (systemName) { case "HitBox": CombatSystem.Instance.HitBoxSystem.Invoke(methodName, 0); break; case "Camera": CameraSystem.Instance.Invoke(methodName, 0); break; } } 二、地图系统的实现 1. 设计相关 地图构成上相对简单,即据点地图为固定地图,关卡内地图部分区域随机生成 2. 实现相关 地图系统的实现上有几个要点: - 以网格为基础,构建网格、世界坐标相互转换的坐标体系,以实现单纯离散网格能与连续空间对应 - 以数据驱动网格内容设计,如 - 是否可以通过、草地雪地土地等几乎不会变化的基础信息,可以直接预先生成 - 天气对地形有影响、解密或奖励机关的状态改变对地形有影响等相对低频次触发的变化信息,可以较低频率的同步 - 角色等对象占用或有临时出现的障碍地形之类相对高频触发的变化信息,可以直接使用事件驱动 - 通过数据结构化网格以实现动态网格加载内容,以确保场景无关性 - 导航系统 - 优化相关 1. 简单构建双向转换坐标系,如: public Vector3 GridToWorld(int x, int y) { return new Vector3 ( x * cellSize + cellSize/2, 0, y * cellSize + cellSize/2 ); } public Vector2Int WorldToGrid(Vector3 pos) { return new Vector2Int ( Mathf.FloorToInt(pos.x / cellSize), Mathf.FloorToInt(pos.z / cellSize) ); } 2. 导航系统可以优先检测碰撞体再使用网格导航,如: //分帧处理 private IEnumerator CalculateWalkableSurface() { const float cellResolution = 0.5f; int gridDimension = (int)(gridSize / cellResolution); for (int y = 0; y < rowCount * gridDimension; y++) { for (int x = 0; x < columnCount * gridDimension; x++) { Vector3 scanOrigin = transform.position + new Vector3(x * cellResolution, -10, y * -cellResolution); bool isObstructed = Physics.CheckSphere(scanOrigin, collisionCheckRadius, obstacleLayer); if (!isObstructed && NavMesh.SamplePosition(scanOrigin, out NavMeshHit hit, sampleDistance, walkableAreaMask)) { Debug.DrawLine(hit.position, hit.position + Vector3.up, Color.green, visualizationDuration); } } yield return null; } } 3. 优化策略相对比较多 - 可以给区域加权使其优先加载,以此实现例如只加载方圆5km内的区域 - 对资源的生命周期追踪管理及时卸载 - 可以通过些预见性或数据,优先保证某些区域的加载,如: public void AdjustLoadingOrder(ScenePartition[] partitions) { Array.Sort(partitions, (a,b) => { float priorityA = CalculatePriority(a); float priorityB = CalculatePriority(b); return priorityB.CompareTo(priorityA); }); } private float CalculatePriority(ScenePartition partition) { // 基于可见性、玩家位置、历史进入数据、玩家行为预测等优先加载 return ...; } 三、NPC系统的实现 1. 设计相关 NPC在据点中,在进攻事件发生前主要以加速建造功能为主,在进攻事件发生后,则会参与战斗。在关卡内刷新随机的NPC,发现后可以加入据点。 2. 实现相关 NPC系统的实现更多是AI架构的实现,测试过后相对好的方案是使用FSM状态机来进行宏观状态的维护,例如NPC正在战斗/建造/休息等,实际战斗内具体的动作行为,再通过Behavior Tree进行处理,节点设计例如: Selector 技能释放 Sequence 连段派生 Parallel 锁定目标并向其移动 Decorator 概率触发的特技或持续检测冷却时间 可以使用封装好的RVO算法做NPC的导航避障。 四、技能系统的实现 1. 设计相关 技能相关的部分为了可视化编辑,可以考虑使用ScriptableObject进行制作,尽量使技能系统既能数据驱动也能事件驱动,有相对可观且便捷的扩展性 2. 实现相关 1. 需要将技能系统数据结构化,以实现多样需求,如: public class SkillData : ScriptableObject { public string skillName; // 技能名称 public float cooldown = 1f; // 冷却时间 public Sprite icon; // 技能图标 public List<SkillPhase> phases = new List<SkillPhase>(); // 技能阶段 public GameObject castVFX; // 施法特效 public AudioClip castSFX; // 施法音效 } // 技能阶段配置 public class SkillPhase { [Range(0, 5)] public float triggerTime; // 触发时间(秒) public SkillEffectType effectType; // 效果类型 public TargetSelectionStrategy targetStrategy; //目标选择方式 public EffectParams parameters; // 效果参数 } 2. 实际释放技能时再根据技能数据分别处理,如: public class SkillSystem : MonoBehaviour { private SkillData currentSkill; // 当前释放的技能 private float cooldownTimer; // 冷却计时器 private Coroutine castingCoroutine; // 开始释放 public void StartCast(SkillData skill) { if(cooldownTimer > 0 || currentSkill != null) return; currentSkill = skill; castingCoroutine = StartCoroutine(CastingProcess()); } // 施法过程 private IEnumerator CastingProcess() { PlayCastAnimation(); SpawnVFX(currentSkill.castVFX); PlaySFX(currentSkill.castSFX); // 执行技能阶段 foreach(var phase in currentSkill.phases) { yield return new WaitForSeconds(phase.triggerTime); // 获取目标 var targets = SelectTargets(phase.targetStrategy); // 执行 ExecuteEffect(phase.effectType, targets, phase.parameters); } currentSkill = null; cooldownTimer = currentSkill.cooldown; } // 目标选择方式 private List<GameObject> SelectTargets(TargetSelectionStrategy strategy) { // 比如圆形范围选择) switch(strategy) { case TargetSelectionStrategy.Self: return new List<GameObject>{gameObject}; case TargetSelectionStrategy.CircleRange: return Physics.OverlapSphere(transform.position, 5f) .Select(c => c.gameObject).ToList(); default: return new List<GameObject>(); } } 3. 通过不同的效果参数配置,来实现不同的如前摇后摇持续等技能效果,如: public interface ISkillEffect { void Execute(GameObject caster, List<GameObject> targets, EffectParams parameters); } // 伤害效果实现 public class DamageEffect : ISkillEffect { public void Execute(GameObject caster, List<GameObject> targets, EffectParams parameters) { foreach(var target in targets) { if(target.TryGetComponent<HealthSystem>(out var health)) { float finalDamage = parameters.baseValue + (parameters.isPercentDamage ? health.maxHP * parameters.percentValue : 0); //造成伤害 } } } } } 三、资源管理方案 如何使用Addressable完成第三点资源管理方案
最新发布
07-05
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值