玩家控制、创建、出生点与动画模块 private

玩家控制、创建、出生点与动画模块

1. 模块边界

玩家不是一个单独的 MonoBehaviour,而是三层对象组合:

层真实类型/入口作用
ECS 逻辑实体CoreEntity + NetworkEntityID、位置、玩家 ID、Hero 标记、同步数据
视图对象GameObjectEntity + Hero/ActorEntityPrefab、碰撞、NavMeshAgent、技能和表现
业务数据Game.Hero/MainPlayer属性、背包、技能、玩家持久化数据

关键文件:

  • 输入:Assets/Code/Game/OLogic/Systems/PlayerInputSystem.cs、Assets/Code/Game/Temp/ApplyPlayerInputSystem.cs
  • 创建:Assets/Code/Game/OGame/Map/Systems/SpawnHeroSystem.cs、Assets/Code/Game/OLogic/Systems/SingleplayerHeroSpawnSystem.cs
  • 摆点:Assets/Maps/Scripts/Editor/ObjectSpawnPoint.cs、Assets/Maps/Scripts/Editor/HeroSpawnRandomGroup.cs
  • 视图绑定:Assets/Code/Game/OLogic/LocalLogicObject.cs、Assets/Code/Game/OLogic/Actor/Actor/ActorEntity.cs
  • 动画:Assets/Code/Game/OLogic/Actor/Animator/IAnimatorController.cs、AnimatorController.cs、Assets/Code/Game/OLogic/Actor/Hero/HeroView.cs
  • 具体英雄:Assets/Code/Game/OLogic/Actor/Hero/Concrete/*HeroController.cs、*HeroStateMachine.cs

2. 玩家创建时序

sequenceDiagram
    participant Map as MapHeroSpawner
    participant Spawn as SpawnHeroSystem
    participant ECS as CoreContext
    participant View as ObjectFactory/GameObjectEntity
    participant Hero as Hero
    participant Input as PlayerInputSystem
    Map->>Spawn: SpawnHeroEvent
    Spawn->>ECS: CreateEntity + Clone blueprint
    Spawn->>ECS: CoreId/Level/ObjectType/PlayerId
    ECS->>View: 创建 Hero Prefab 并绑定 CoreEntity
    View->>Hero: OnEntityCreate(ActorCreateData)
    Hero->>Hero: 注入 Move/Collision/Animator
    Hero->>Hero: 具体 HeroController.Init(StateMachine)
    Input->>Input: TryToUpdateMoveDir -> SyncUpdateMoveDir

3. 网络模式如何创建玩家

SpawnHeroSystem 监听两个 ECS Group:

_heroSpawnerGroup = contexts.map.GetGroup(
    MapMatcher.AllOf(MapMatcher.HeroSpawner, MapMatcher.Spawner));
_heroSpawnEventGroup = contexts.map.GetGroup(MapMatcher.SpawnHeroEvent);
_networkHeroGroup = contexts.network.GetGroup(
    NetworkMatcher.AllOf(NetworkMatcher.Local, NetworkMatcher.PlayerId));

第一次 SpawnHeroEvent 创建玩家,后续事件只把已有玩家 Relocate 到新的出生位置:

spawnStream.First()
    .Subscribe(t => Spawn(t.blueprint, t.@event));
spawnStream.Skip(1)
    .Subscribe(t => Relocate(t.blueprint, t.@event));

void Spawn(CoreEntity blueprint, SpawnHeroEvent @event)
{
    var hero = _coreContext.CreateEntity();
    blueprint.CloneTo(hero);
    hero.AddCoreId(_coreId.Next(CoreType.Hero));
    hero.AddLevel(@event.Level);
    hero.ReplacePosition(hero.position.Value + @event.Offset);
    hero.AddObjectType(@event.HeroType);
    hero.ReplacePlayerId(_networkHeroGroup.GetSingleEntity().playerId.Value);
}

单机模式使用 SingleplayerHeroSpawnSystem,只监听 HeroSpawner + Spawner,并把蓝图复制为本地 CoreEntity;因此单机不会依赖 Login/Lobby/Battle 的网络玩家 ID。

4. 玩家出生点怎么摆

4.1 固定出生点

在地图场景中创建带 ObjectSpawnPoint 的节点,Type 选择 Hero 相关 ObjectType。ObjectSpawnPoint 的 Type 改变时会:

  1. 从 CfgObject/CfgMonster 等生成配置中刷新可选项。
  2. 更新 SpawnName/SpawnId。
  3. 刷新预览子物体。
  4. 在 Bake/Gather 时把该点写入地图数据。

ObjectSpawnPoint 的有效类型由枚举名过滤得到,Hero、Monster、Boss、Gpe、Door、Guard、WaveSpawner 等都会进入编辑器下拉框。

4.2 多英雄随机出生组

HeroSpawnRandomGroup 是编辑器容器,子节点就是候选模板,每个模板保存 Guid、Child 和 Weight:

[Range(0, 1)] public float Weight = 1;

void OnPreviewChanged()
{
    foreach (var t in _templates)
        t.Child.SetActive(t.Name == _preview);
}

注意:Preview 只控制编辑器显示哪个子节点,不决定运行时随机结果。运行时应使用导出的 Guid/配置和权重。

4.3 摆点验收

  • 出生点位于目标地图的 MapRoot 之下。
  • 旋转方向代表玩家初始朝向,Offset 只用于事件微调。
  • 点位必须落在可行走 NavMesh,不能只看模型表面。
  • 多人出生点要分开,避免 Agent 初始重叠。
  • 导出前运行 GatherCurrSceneGPE/ZipCurrMapData,并检查导出的点位数量。

5. 输入到移动

PlayerInputSystem 不直接改 Transform,它只为本地 Hero 的消息补充 EntityId:

private IGroup<NetworkEntity> _heros;

public PlayerInputSystem(MessageCenter messageCenter,
    NetworkContext networkContext)
{
    _messageCenter = messageCenter;
    _heros = networkContext.GetGroup(
        NetworkMatcher.AllOf(NetworkMatcher.HeroEntity, NetworkMatcher.Local));
}

void TransformMessageWithEntityID(LogicMessageType from,
    LogicMessageType to) => _messageCenter
    .ObserveOn(from)
    .Select(message => (data: message.Data, entity: GetHero()))
    .Where(t => t.entity != null && t.entity.hasEntityID)
    .Subscribe(t => _messageCenter.PostMessage(
        to, t.data, t.entity.entityID.Value));

ApplyPlayerInputSystem 接收 SyncUpdateMoveDir 后计算目标点:

float3 destination = hero.transform.position + finalDir * moveDis;
destination.y = hero.transform.position.y;

if (finalDir == Vector3.zero)
    return float3.zero;
return destination;

输入方向会按当前摄像机方向修正;摇杆反向 Buff 会反转 MoveDir;鼠标点地面模式则通过 ScreenPointToRay 计算方向。最终调用 Hero.MoveController.SetMoveDestination,真正移动由 NavMove 策略和 NavMeshAgent 执行。

技能输入同样经过 EntityId 路由,最终调用 hero.LaunchSkill,而不是由 UI 直接操作 Animator。

6. 玩家对象如何绑定逻辑和视图

LocalLogicObject 在 GameObjectEntity 出现时创建 ActorCreateData:

IAnimatorController animCtrl =
    WorldManager.Instance.WorldData.GameplayConfig.ExperimentalAnimatorController
        ? new ExperimentalAnimatorController()
        : new AnimatorController();

var actorData = new ActorCreateData(
    true,
    new SyncDesMoveController(LogicMessageType.SyncMoveData,
        gameObject.linkedCore.Entity.entityID.Value),
    new CollisionController(),
    animCtrl);

entityObject.Entity = gameObject.linkedCore.Entity;
entityObject.GameObjectEntity = gameObject;
entityObject.OnEntityCreate(actorData);
gameObject.AddViewObject(entityObject);
gameObject.isViewControlled = true;

ActorEntity.OnEntityCreate 会设置本地标记、移动控制器、碰撞控制器和动画控制器,并从 CoreEntity 设置 EntityCode。Hero 组件本身要求存在 HeroAttribute、HeroView、Inventory、HeroSkillVariation。

7. Animator 是怎么控制的

7.1 控制器抽象

IAnimatorController 把状态机和业务解耦,业务只调用:

void PlayAnimation(string stateName, string parameterName = null);
void PlayAnimation(string stateName, string parameterName, int value);
void PlayGroupAnimation(string subStateMachineName,
    string parameterName, int value);
void FinishPlayAnimation();
bool IsCurrentAnimationPlaying(int layerIndex = 0);

默认实现 AnimatorController 持有 Unity Animator,使用 StringToHash 缓存目标状态,并支持 Trigger、Integer、Float、Bool 四种参数。

7.2 状态进入/退出回调

Init 时从 Animator 取 AnimStateBehaviour 和 AnimGroupBehaviour,并注册 Enter/Exit 回调:

_animator = animator;
_baseLayerName = _animator.GetLayerName(0);
var behavior = _animator.GetBehaviour<AnimStateBehaviour>();
behavior.OnStateEnterAction += OnAnimatorStateEnter;
behavior.OnStateExitAction += OnAnimatorStateExit;

因此 IsCurrentAnimationPlaying/AutoFinished 不是通过猜时间,而是由状态机行为回调维护状态表。

7.3 HeroView 与 Animator Controller 资源

HeroView 持有:

[SerializeField] Animator _animator;
public RuntimeAnimatorController Controller_Default;
public RuntimeAnimatorController Controller_VariationA;
public RuntimeAnimatorController Controller_VariationB;
public RuntimeAnimatorController Controller_VariationC;
public RuntimeAnimatorController Controller_VariationD;

换肤或技能变体时切换 RuntimeAnimatorController;播放动作仍通过 IAnimatorController,避免业务代码散落 animator.SetTrigger。

7.4 具体英雄初始化

例如 Boy01_HeroController.Start:

_heroInstance = GetComponent<Hero>();
var stateMachine = _heroInstance.GetStateMachine<Boy01_HeroStateMachine>();
_heroInstance.Init(stateMachine);
stateMachine.Init(_heroInstance,
    Boy01_HeroStateMachine.StateType.Idle);
_heroInstance.MoveController.DefaultMoveStrategy = MoveStrategyType.NavMove;
_heroInstance.MoveController.ChangeMoveStrategy(MoveStrategyType.NavMove);

之后 HeroStateMachine 根据 Idle、Run、Attack、Hurt、Dead 等业务状态调用 AnimatorController.PlayAnimation。Boss/Enemy 使用同一套抽象,区别只是各自的 StateMachine 和 Animator Controller 资源。

8. 新增一个可控制玩家

  1. 准备 Hero Prefab:Hero、HeroView、HeroAttribute、Inventory、HeroSkillVariation、NavMeshAgent,以及模型上的 Animator。
  2. 在 HeroView 填好 Default/Variation Controller,并确认 Animator Controller 含 AnimStateBehaviour。
  3. 新建 XxxHeroController 和 XxxHeroStateMachine,先实现 Idle/Run/Dead 三个状态。
  4. 在 ObjectType、Prefab 映射和配置表登记 HeroType。
  5. 把 Prefab 放入 HeroSpawnRandomGroup 或固定 ObjectSpawnPoint。
  6. 执行地图 Gather/Bake,确认导出出生点。
  7. 启动本地服或单机,验证输入、NavMesh、动画和断线重连。
  8. 任何新增动画状态都同时补 StateMachine 调用、Animator 参数和 Enter/Exit 行为。

9. 玩家问题排错表

现象首查
没有玩家实体MapHeroSpawner、SpawnHeroEvent、SpawnHeroSystem
有实体无模型GameObjectEntity/ObjectFactory、Prefab 映射、HeroView
模型出现但不动PlayerInputSystem、ApplyPlayerInputSystem、MoveController、NavMeshAgent
玩家掉出地图出生点是否在 NavMesh、Agent areaMask、IsOnNavMesh
能移动但不播 RunXxxHeroController 是否 Init、StateMachine 是否调用 PlayAnimation(“Run”)
技能动作卡住AnimStateBehaviour、FinishPlayAnimation、Animator 参数名
远端玩家动画异常RemoteLogicObject 使用的 AnimatorController、快照状态和插值逻辑