玩家控制、创建、出生点与动画模块 private
玩家控制、创建、出生点与动画模块
1. 模块边界
玩家不是一个单独的 MonoBehaviour,而是三层对象组合:
| 层 | 真实类型/入口 | 作用 |
|---|---|---|
| ECS 逻辑实体 | CoreEntity + NetworkEntity | ID、位置、玩家 ID、Hero 标记、同步数据 |
| 视图对象 | GameObjectEntity + Hero/ActorEntity | Prefab、碰撞、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 改变时会:
- 从 CfgObject/CfgMonster 等生成配置中刷新可选项。
- 更新 SpawnName/SpawnId。
- 刷新预览子物体。
- 在 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. 新增一个可控制玩家
- 准备 Hero Prefab:Hero、HeroView、HeroAttribute、Inventory、HeroSkillVariation、NavMeshAgent,以及模型上的 Animator。
- 在 HeroView 填好 Default/Variation Controller,并确认 Animator Controller 含 AnimStateBehaviour。
- 新建 XxxHeroController 和 XxxHeroStateMachine,先实现 Idle/Run/Dead 三个状态。
- 在 ObjectType、Prefab 映射和配置表登记 HeroType。
- 把 Prefab 放入 HeroSpawnRandomGroup 或固定 ObjectSpawnPoint。
- 执行地图 Gather/Bake,确认导出出生点。
- 启动本地服或单机,验证输入、NavMesh、动画和断线重连。
- 任何新增动画状态都同时补 StateMachine 调用、Animator 参数和 Enter/Exit 行为。
9. 玩家问题排错表
| 现象 | 首查 |
|---|---|
| 没有玩家实体 | MapHeroSpawner、SpawnHeroEvent、SpawnHeroSystem |
| 有实体无模型 | GameObjectEntity/ObjectFactory、Prefab 映射、HeroView |
| 模型出现但不动 | PlayerInputSystem、ApplyPlayerInputSystem、MoveController、NavMeshAgent |
| 玩家掉出地图 | 出生点是否在 NavMesh、Agent areaMask、IsOnNavMesh |
| 能移动但不播 Run | XxxHeroController 是否 Init、StateMachine 是否调用 PlayAnimation(“Run”) |
| 技能动作卡住 | AnimStateBehaviour、FinishPlayAnimation、Animator 参数名 |
| 远端玩家动画异常 | RemoteLogicObject 使用的 AnimatorController、快照状态和插值逻辑 |