T03 --- 角色基类与移动系统 详细功能设计文档
文档版本 : 2.0
创建日期 : 2026-07-16
最后修订 : 2026-07-17(基于审阅报告全面完善,9 项设计决策落地)
关联任务 : T03(角色基类与移动系统)
前置任务 : T01(项目基础搭建)、T02(俯视角摄像机系统)
后续任务: T04(GAS 集成基础)、T10(手机端输入适配)
一、任务概述
| 项目 | 内容 |
|---|---|
| 任务ID | T03 |
| 任务名称 | 角色基类与移动系统 |
| 复杂度 | ⭐⭐ 中等 |
| 前置依赖 | T01(项目基础搭建)已完成、T02(俯视角摄像机系统)已完成 |
| 产出物 | Character 基类、玩家角色类、AI角色空壳、输入系统(InputConfig + InputComponent)、移动逻辑、动画蓝图 |
目标描述
实现角色基类 AXiYouCharacter 和玩家角色 AXiYouPlayerCharacter,配置俯视角移动系统(WASD 移动 + 左键点击地面移动),创建 AI 角色空壳占位,实现基础动画状态机(Idle/Walk/Run),预留 GAS 接口供 T04 填充。
本版本引入 EnhancedInput + InputConfig DataAsset + GameplayTag 驱动 的输入系统架构(参考 Lyra 简化版),实现输入与逻辑的解耦。所有移动逻辑使用 UE5 内置 UCharacterMovementComponent 的网络同步机制,不自定义移动同步。
里程碑归属
本任务属于 里程碑 1: 基础框架就绪(T01 + T02 + T03)。完成后应满足:
- UE5 项目编译运行
- 俯视角摄像机正常工作(T02 已完成)
- 角色能通过 WASD / 左键点击地面移动
- DS 模式下移动同步正常
- 角色有基础 Idle/Walk/Run 动画
设计决策汇总
本版本基于审阅报告确认的 9 项设计决策进行全面修订:
| # | 决策项 | 确认结果 | 落地章节 |
|---|---|---|---|
| 1 | 点击移动按键 | 左键移动(与 PRD 右键要求有意偏差,见 §3.3 说明) | §3.3 |
| 2 | ASC 预留位置 | 移到 PlayerState 上,Character 只保留转发函数 | §2.2、§2.5 |
| 3 | Death 动画 | T03 不加,T05/T07 实现,当前只做 Idle/Walk/Run | §4.1 |
| 4 | 头顶血条 | T03 不做,完全留给 T11,不预留组件 | §2.2 |
| 5 | 输入系统 | EnhancedInput + InputConfig DataAsset + GameplayTag 驱动(Lyra 简化版) | §3.2、§6 |
| 6 | WASD 方向基准 | 世界坐标方向(W=+X前, S=-X后, A=-Y左, D=+Y右) | §3.2 |
| 7 | 寻路 API | SimpleMoveToLocation,处理不可达边界场景 | §3.3 |
| 8 | 网络延迟 | 客户端本地预测移动 + 服务端校正 | §3.3、§5.3 |
| 9 | 其他改进 | 移除 AutoPossessPlayer、移除 bUseSeamlessTravel、清理空函数、补充网络配置、动画用免费资源 | 全文 |
二、类设计
2.1 类继承关系图
#mermaid-svg-HukfK128H07E8QDS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HukfK128H07E8QDS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HukfK128H07E8QDS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HukfK128H07E8QDS .error-icon{fill:#552222;}#mermaid-svg-HukfK128H07E8QDS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HukfK128H07E8QDS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HukfK128H07E8QDS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HukfK128H07E8QDS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HukfK128H07E8QDS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HukfK128H07E8QDS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HukfK128H07E8QDS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HukfK128H07E8QDS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HukfK128H07E8QDS .marker.cross{stroke:#333333;}#mermaid-svg-HukfK128H07E8QDS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HukfK128H07E8QDS p{margin:0;}#mermaid-svg-HukfK128H07E8QDS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HukfK128H07E8QDS .cluster-label text{fill:#333;}#mermaid-svg-HukfK128H07E8QDS .cluster-label span{color:#333;}#mermaid-svg-HukfK128H07E8QDS .cluster-label span p{background-color:transparent;}#mermaid-svg-HukfK128H07E8QDS .label text,#mermaid-svg-HukfK128H07E8QDS span{fill:#333;color:#333;}#mermaid-svg-HukfK128H07E8QDS .node rect,#mermaid-svg-HukfK128H07E8QDS .node circle,#mermaid-svg-HukfK128H07E8QDS .node ellipse,#mermaid-svg-HukfK128H07E8QDS .node polygon,#mermaid-svg-HukfK128H07E8QDS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HukfK128H07E8QDS .rough-node .label text,#mermaid-svg-HukfK128H07E8QDS .node .label text,#mermaid-svg-HukfK128H07E8QDS .image-shape .label,#mermaid-svg-HukfK128H07E8QDS .icon-shape .label{text-anchor:middle;}#mermaid-svg-HukfK128H07E8QDS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HukfK128H07E8QDS .rough-node .label,#mermaid-svg-HukfK128H07E8QDS .node .label,#mermaid-svg-HukfK128H07E8QDS .image-shape .label,#mermaid-svg-HukfK128H07E8QDS .icon-shape .label{text-align:center;}#mermaid-svg-HukfK128H07E8QDS .node.clickable{cursor:pointer;}#mermaid-svg-HukfK128H07E8QDS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HukfK128H07E8QDS .arrowheadPath{fill:#333333;}#mermaid-svg-HukfK128H07E8QDS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HukfK128H07E8QDS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HukfK128H07E8QDS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HukfK128H07E8QDS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HukfK128H07E8QDS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HukfK128H07E8QDS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HukfK128H07E8QDS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HukfK128H07E8QDS .cluster text{fill:#333;}#mermaid-svg-HukfK128H07E8QDS .cluster span{color:#333;}#mermaid-svg-HukfK128H07E8QDS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HukfK128H07E8QDS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HukfK128H07E8QDS rect.text{fill:none;stroke-width:0;}#mermaid-svg-HukfK128H07E8QDS .icon-shape,#mermaid-svg-HukfK128H07E8QDS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HukfK128H07E8QDS .icon-shape p,#mermaid-svg-HukfK128H07E8QDS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HukfK128H07E8QDS .icon-shape .label rect,#mermaid-svg-HukfK128H07E8QDS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HukfK128H07E8QDS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HukfK128H07E8QDS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HukfK128H07E8QDS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 实现 IAbilitySystemInterface
转发 ASC 获取
ACharacter
AXiYouCharacter
AXiYouPlayerCharacter
AXiYouAICharacter
APlayerState
AXiYouPlayerState
IAbilitySystemInterface
2.2 AXiYouCharacter(角色基类)
| 项目 | 内容 |
|---|---|
| 类名 | AXiYouCharacter |
| 父类 | ACharacter |
| 头文件 | Source/XiYou/Public/Character/XiYouCharacter.h |
| 源文件 | Source/XiYou/Private/Character/XiYouCharacter.cpp |
| UCLASS | UCLASS(BlueprintType) |
组件构成
| 组件 | 类型 | 来源 | 说明 |
|---|---|---|---|
| CapsuleComponent | UCapsuleComponent |
ACharacter 自带 | 碰撞体,无需额外创建 |
| Mesh | USkeletalMeshComponent |
ACharacter 自带 | 网格体,无需额外创建 |
| CharacterMovement | UCharacterMovementComponent |
ACharacter 自带 | 移动组件,构造函数直接配置 |
设计决策 #2 落地 :ASC 和 AttributeSet 不在 Character 上创建实例 。Character 只保留
GetAbilitySystemComponent()转发函数,通过GetPlayerState()获取 PlayerState 上的 ASC。详见 §2.5。
设计决策 #4 落地 :不添加 UWidgetComponent(头顶血条完全留给 T11 实现)。
头文件设计
cpp
// XiYouCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AbilitySystemInterface.h" // IAbilitySystemInterface 接口
#include "XiYouCharacter.generated.h"
// 前向声明
class UAbilitySystemComponent;
class UAttributeSet;
class UAnimMontage;
class AXiYouPlayerState;
/**
* AXiYouCharacter - 角色基类
* 包含核心组件和通用逻辑,所有角色(玩家/AI)的基类
* ASC 挂载在 PlayerState 上,Character 通过 GetAbilitySystemComponent() 转发获取
*/
UCLASS(BlueprintType)
class XIYOU_API AXiYouCharacter : public ACharacter, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AXiYouCharacter();
// ===== 生命周期 =====
virtual void BeginPlay() override;
virtual void PossessedBy(AController* NewController) override;
// ===== GAS 接口(IAbilitySystemInterface 实现) =====
/**
* 获取 AbilitySystemComponent - 转发到 PlayerState
* T03 阶段 PlayerState 上的 ASC 为 nullptr,T04 填充
* @return PlayerState 上的 ASC 指针,无 PlayerState 时返回 nullptr
*/
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
// ===== 移动相关 =====
/** 获取当前移动速度(用于动画状态机驱动) */
UFUNCTION(BlueprintPure, Category = "Movement")
float GetMovementSpeed() const;
/** 是否正在移动 */
UFUNCTION(BlueprintPure, Category = "Movement")
bool IsMoving() const;
protected:
// ===== 动画相关 =====
/** 角色使用的动画蓝图类(子类可覆盖) */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Animation")
TSubclassOf<UAnimInstance> AnimClass;
};
源文件设计
cpp
// XiYouCharacter.cpp
#include "Character/XiYouCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PlayerState.h"
#include "Core/XiYouPlayerState.h"
AXiYouCharacter::AXiYouCharacter()
{
// ===== 俯视角移动配置(构造函数直接配置,无独立 ConfigureMovementComponent 函数) =====
// 不使用控制器旋转(俯视角固定方向)
bUseControllerRotationYaw = false;
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
// 角色朝向移动方向
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->bUseControllerDesiredRotation = false;
// 旋转速度 - 俯视角需要快速转向
GetCharacterMovement()->RotationRate = FRotator(0.0f, 720.0f, 0.0f);
// 移动模式
GetCharacterMovement()->MovementMode = MOVE_Walking;
// 网络平滑 - 减少客户端预测误差带来的视觉抖动
GetCharacterMovement()->NetworkSmoothingMode = ENetworkSmoothingMode::Exponential;
GetCharacterMovement()->NetworkSimulatedSmoothLocationTime = 0.1f;
GetCharacterMovement()->NetworkSimulatedSmoothRotationTime = 0.1f;
// 不需要跳跃
GetCharacterMovement()->JumpZVelocity = 0.0f;
GetCharacterMovement()->AirControl = 0.0f;
// 导航网格移动配置
GetCharacterMovement()->bUseFixedBrakingDistance = true;
// 默认无动画蓝图类,子类或蓝图设置
AnimClass = nullptr;
}
void AXiYouCharacter::BeginPlay()
{
Super::BeginPlay();
}
void AXiYouCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// T04 将在此处通过 PlayerState 初始化 ASC
// 当前 T03 阶段 ASC 为 nullptr,无需操作
}
UAbilitySystemComponent* AXiYouCharacter::GetAbilitySystemComponent() const
{
// 设计决策 #2:ASC 挂载在 PlayerState 上
// Character 通过转发获取,不直接持有 ASC 指针
if (const AXiYouPlayerState* PS = Cast<AXiYouPlayerState>(GetPlayerState()))
{
return PS->GetAbilitySystemComponent();
}
return nullptr;
}
float AXiYouCharacter::GetMovementSpeed() const
{
return GetVelocity().Length();
}
bool AXiYouCharacter::IsMoving() const
{
return GetMovementSpeed() > 1.0f;
}
设计决策 #9 落地 :移除了原
ConfigureMovementComponent()空函数,构造函数直接配置移动参数。移除了AbilitySystemComponent/AttributeSet成员指针声明。移除了OnMoveCompleted空回调。
2.3 AXiYouPlayerCharacter(玩家角色)
| 项目 | 内容 |
|---|---|
| 类名 | AXiYouPlayerCharacter |
| 父类 | AXiYouCharacter |
| 头文件 | Source/XiYou/Public/Character/XiYouPlayerCharacter.h |
| 源文件 | Source/XiYou/Private/Character/XiYouPlayerCharacter.cpp |
| UCLASS | UCLASS(BlueprintType) |
头文件设计
cpp
// XiYouPlayerCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "Character/XiYouCharacter.h"
#include "XiYouPlayerCharacter.generated.h"
// 前向声明
struct FInputActionValue;
/**
* AXiYouPlayerCharacter - 玩家角色
* 处理玩家输入(WASD移动、点击移动)
* 继承自 AXiYouCharacter
*
* 注意:输入绑定由 PlayerController 统一管理(Tag 驱动架构)
* 角色类只提供移动执行函数供 Controller 调用
*/
UCLASS(BlueprintType)
class XIYOU_API AXiYouPlayerCharacter : public AXiYouCharacter
{
GENERATED_BODY()
public:
AXiYouPlayerCharacter();
// ===== 点击移动执行 =====
/**
* 点击地面移动 - 由 PlayerController 调用
* 使用 NavMesh SimpleMoveToLocation 寻路
* @param TargetLocation 目标位置(NavMesh 投影后的世界坐标)
*/
void MoveToLocation(const FVector& TargetLocation);
/** 停止点击移动(WASD 输入时调用,取消寻路) */
void StopClickMove();
};
源文件设计
cpp
// XiYouPlayerCharacter.cpp
#include "Character/XiYouPlayerCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "NavigationSystem.h"
AXiYouPlayerCharacter::AXiYouPlayerCharacter()
{
// 设计决策 #9:移除 AutoPossessPlayer = EAutoReceiveInput::Player0
// 依赖 GameMode 自动 Possess(GameMode 的 DefaultPawnClass 机制自动处理)
}
void AXiYouPlayerCharacter::MoveToLocation(const FVector& TargetLocation)
{
// 使用 NavMesh 寻路移动到目标点
if (UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld()))
{
if (AController* CharController = GetController())
{
// 停止当前移动
GetCharacterMovement()->StopMovementImmediately();
// 使用 SimpleMoveToLocation 进行基于 NavMesh 的移动
NavSys->SimpleMoveToLocation(CharController, TargetLocation);
}
}
}
void AXiYouPlayerCharacter::StopClickMove()
{
// 取消基于 NavMesh 的寻路移动
if (AController* CharController = GetController())
{
CharController->StopMovement();
}
}
设计决策 #9 落地 :移除了
AutoPossessPlayer = EAutoReceiveInput::Player0,依赖 GameMode 自动 Possess。移除了角色类中的输入绑定逻辑(MoveAction、InputMappingContext、SetupPlayerInputComponent、OnMoveTriggered、OnMoveCompleted),输入绑定统一由 PlayerController 通过 Tag 驱动架构管理。
2.4 AXiYouAICharacter(AI角色基类 --- 空壳占位)
| 项目 | 内容 |
|---|---|
| 类名 | AXiYouAICharacter |
| 父类 | AXiYouCharacter |
| 头文件 | Source/XiYou/Public/Character/XiYouAICharacter.h |
| 源文件 | Source/XiYou/Private/Character/XiYouAICharacter.cpp |
| UCLASS | UCLASS() |
| 说明 | 仅创建空壳占位文件,不实现任何逻辑,留到 T09 实现 |
头文件设计
cpp
// XiYouAICharacter.h
#pragma once
#include "CoreMinimal.h"
#include "Character/XiYouCharacter.h"
#include "XiYouAICharacter.generated.h"
/**
* AXiYouAICharacter - AI角色基类(怪物/NPC)
* T03 阶段仅创建空壳占位,不实现逻辑
* T09 将实现 AIController 集成、行为树、仇恨系统
*/
UCLASS()
class XIYOU_API AXiYouAICharacter : public AXiYouCharacter
{
GENERATED_BODY()
public:
AXiYouAICharacter();
// T09 将在此处添加 AI 相关逻辑
// - AIController 绑定
// - 行为树资产引用
// - 仇恨系统
// - AI 属性配置
};
源文件设计
cpp
// XiYouAICharacter.cpp
#include "Character/XiYouAICharacter.h"
AXiYouAICharacter::AXiYouAICharacter()
{
// T09 将在此处配置 AI 相关参数
// 当前为空壳,不实现任何逻辑
}
2.5 AXiYouPlayerState 修改(ASC 预留)
| 项目 | 内容 |
|---|---|
| 类名 | AXiYouPlayerState |
| 父类 | APlayerState |
| 头文件 | Source/XiYou/Public/Core/XiYouPlayerState.h |
| 源文件 | Source/XiYou/Private/Core/XiYouPlayerState.cpp |
| 修改类型 | 添加 ASC/AttributeSet 预留 + 实现 IAbilitySystemInterface |
设计决策 #2 落地:ASC 和 AttributeSet 移到 PlayerState 上。T03 阶段为空壳(指针为 nullptr),T04 填充实例。
头文件修改
cpp
// XiYouPlayerState.h - 修改后
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "AbilitySystemInterface.h" // IAbilitySystemInterface 接口
#include "XiYouPlayerState.generated.h"
// 前向声明
class UAbilitySystemComponent;
class UAttributeSet;
/**
* XiYouPlayerState - 玩家状态
* 挂载 AbilitySystemComponent 和 AttributeSet(T04 填充)
* 作为 ASC 的 Owner Actor 和 Avatar Actor
*/
UCLASS()
class XIYOU_API AXiYouPlayerState : public APlayerState, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AXiYouPlayerState();
// ===== IAbilitySystemInterface 实现 =====
/** 获取 AbilitySystemComponent,T04 填充后返回有效指针 */
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
protected:
// ===== GAS 组件指针(T04 填充实例) =====
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities", meta = (AllowPrivateAccess = "true"))
UAbilitySystemComponent* AbilitySystemComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities", meta = (AllowPrivateAccess = "true"))
UAttributeSet* AttributeSet;
};
源文件修改
cpp
// XiYouPlayerState.cpp - 修改后
#include "Core/XiYouPlayerState.h"
AXiYouPlayerState::AXiYouPlayerState()
{
// GAS 组件初始化为 nullptr,T04 创建实例
AbilitySystemComponent = nullptr;
AttributeSet = nullptr;
}
UAbilitySystemComponent* AXiYouPlayerState::GetAbilitySystemComponent() const
{
// T03 阶段返回 nullptr,T04 返回有效指针
return AbilitySystemComponent;
}
2.6 输入系统类设计(新增)
UXiYouInputConfig(DataAsset)
| 项目 | 内容 |
|---|---|
| 类名 | UXiYouInputConfig |
| 父类 | UDataAsset |
| 头文件 | Source/XiYou/Public/Core/XiYouInputConfig.h |
| 源文件 | Source/XiYou/Private/Core/XiYouInputConfig.cpp |
| 设计模式 | 数据驱动配置模式 --- DataAsset 存储 InputAction → GameplayTag 映射 |
参考
ue5-input-system技能的 Lyra 架构,简化版(不用 GameFeature 动态 IMC + 状态机初始化)。
头文件设计
cpp
// XiYouInputConfig.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "XiYouInputConfig.generated.h"
// 前向声明
class UInputAction;
/**
* 单个输入动作与 GameplayTag 的映射条目
*/
USTRUCT(BlueprintType)
struct FXiYouInputActionConfig
{
GENERATED_BODY()
/** 输入动作资产(IA_Move, IA_ClickMove 等) */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
UInputAction* InputAction = nullptr;
/** 关联的 GameplayTag(InputTag.Move, InputTag.ClickMove 等) */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input", Meta = (Categories = "InputTag"))
FGameplayTag InputTag;
};
/**
* UXiYouInputConfig - 输入配置数据资产
* 存储 InputAction → GameplayTag 映射,分 Native 和 Ability 两个通道
*
* - Native 通道:直接调用 C++ 函数(如移动、点击移动)
* - Ability 通道:通过 GAS 激活技能(T04+ 使用)
*
* 在蓝图编辑器中创建 DA_XiYouInputConfig 资产并配置映射
*/
UCLASS(BlueprintType, Const)
class XIYOU_API UXiYouInputConfig : public UDataAsset
{
GENERATED_BODY()
public:
/** Native 输入映射(直接调用 C++ 回调) */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TArray<FXiYouInputActionConfig> NativeInputActions;
/** Ability 输入映射(通过 GAS 激活技能,T04+ 使用) */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TArray<FXiYouInputActionConfig> AbilityInputActions;
/**
* 根据 GameplayTag 查找对应的 InputAction
* @param InputTag 要查找的输入标签
* @return 对应的 UInputAction,未找到返回 nullptr
*/
UFUNCTION(BlueprintCallable, Category = "Input")
const UInputAction* FindNativeInputActionForTag(const FGameplayTag& InputTag) const;
/**
* 根据 GameplayTag 查找 Ability 通道的 InputAction
* @param InputTag 要查找的输入标签
* @return 对应的 UInputAction,未找到返回 nullptr
*/
UFUNCTION(BlueprintCallable, Category = "Input")
const UInputAction* FindAbilityInputActionForTag(const FGameplayTag& InputTag) const;
};
源文件设计
cpp
// XiYouInputConfig.cpp
#include "Core/XiYouInputConfig.h"
#include "EnhancedInput/InputAction.h"
const UInputAction* UXiYouInputConfig::FindNativeInputActionForTag(const FGameplayTag& InputTag) const
{
// 遍历 Native 输入映射,查找匹配的 Tag
for (const FXiYouInputActionConfig& Config : NativeInputActions)
{
if (Config.InputAction && Config.InputTag == InputTag)
{
return Config.InputAction;
}
}
return nullptr;
}
const UInputAction* UXiYouInputConfig::FindAbilityInputActionForTag(const FGameplayTag& InputTag) const
{
// 遍历 Ability 输入映射,查找匹配的 Tag
for (const FXiYouInputActionConfig& Config : AbilityInputActions)
{
if (Config.InputAction && Config.InputTag == InputTag)
{
return Config.InputAction;
}
}
return nullptr;
}
UXiYouInputComponent(EnhancedInputComponent 子类)
| 项目 | 内容 |
|---|---|
| 类名 | UXiYouInputComponent |
| 父类 | UEnhancedInputComponent |
| 头文件 | Source/XiYou/Public/Core/XiYouInputComponent.h |
| 源文件 | Source/XiYou/Private/Core/XiYouInputComponent.cpp |
| 设计模式 | 模板方法模式 --- 提供 BindNativeAction 模板方法 |
头文件设计
cpp
// XiYouInputComponent.h
#pragma once
#include "CoreMinimal.h"
#include "EnhancedInputComponent.h"
#include "GameplayTagContainer.h"
#include "XiYouInputComponent.generated.h"
// 前向声明
class UXiYouInputConfig;
/**
* UXiYouInputComponent - 自定义输入组件
* 继承 UEnhancedInputComponent,提供 Tag 驱动的输入绑定模板方法
*
* 使用方式:
* InputComponent->BindNativeAction(InputConfig, InputTag.Move, ETriggerEvent::Triggered, this, &AXiYouPlayerController::HandleMove);
*/
UCLASS()
class XIYOU_API UXiYouInputComponent : public UEnhancedInputComponent
{
GENERATED_BODY()
public:
UXiYouInputComponent();
/**
* 绑定 Native 输入动作(直接调用 C++ 回调)
* @tparam UserClass - 回调所属类
* @tparam FuncType - 回调函数类型
* @param InputConfig - 输入配置资产
* @param InputTag - 输入标签(如 InputTag.Move)
* @param TriggerEvent - 触发事件类型
* @param UserObject - 回调所属对象
* @param Func - 回调函数指针
*/
template <class UserClass, typename FuncType>
void BindNativeAction(const UXiYouInputConfig* InputConfig, const FGameplayTag& InputTag,
ETriggerEvent TriggerEvent, UserClass* UserObject, FuncType Func) const;
};
// 模板方法实现(必须在头文件中)
template <class UserClass, typename FuncType>
void UXiYouInputComponent::BindNativeAction(const UXiYouInputConfig* InputConfig, const FGameplayTag& InputTag,
ETriggerEvent TriggerEvent, UserClass* UserObject, FuncType Func) const
{
if (!InputConfig || !InputTag.IsValid())
{
return;
}
// 从配置中查找 Tag 对应的 InputAction
if (const UInputAction* InputAction = InputConfig->FindNativeInputActionForTag(InputTag))
{
// 绑定 EnhancedInput Action 回调
BindAction(InputAction, TriggerEvent, UserObject, Func);
}
}
源文件设计
cpp
// XiYouInputComponent.cpp
#include "Core/XiYouInputComponent.h"
UXiYouInputComponent::UXiYouInputComponent()
{
// 无额外初始化,继承 UEnhancedInputComponent 默认行为
}
2.7 XiYouGameplayTags(GameplayTag C++ 原生定义)(新增)
| 项目 | 内容 |
|---|---|
| 命名空间 | XiYouGameplayTags |
| 头文件 | Source/XiYou/Public/Core/XiYouGameplayTags.h |
| 源文件 | Source/XiYou/Private/Core/XiYouGameplayTags.cpp |
| 设计模式 | C++ 原生 Tag 定义模式 --- UE_DECLARE_GAMEPLAY_TAG_EXTERN + UE_DEFINE_GAMEPLAY_TAG_COMMENT |
| 参考技能 | ue5-gameplay-tags 技能 §模式1:C++ 原生 Tag 定义模式 |
设计理念 :所有需要在 C++ 代码中直接引用的 Tag 都通过此文件定义,获得编译期类型检查 和 IDE 自动补全。GameplayCue 等不需要 C++ 直接引用的辅助 Tag 仍用 INI 补充。避免硬编码字符串导致的拼写错误。
项目前缀 :XIYOU(对应XIYOU_API宏),命名空间名XiYouGameplayTags。
Tag 命名规范 :遵循四层结构Owner.Category.Specific,T03 阶段只有InputTag类别。C++ 变量命名规则:Tag 字符串中的.用_替代(如InputTag.Move→InputTag_Move)。
头文件设计
cpp
// XiYouGameplayTags.h
#pragma once
#include "NativeGameplayTags.h"
/**
* XiYou 项目 Gameplay Tags C++ 原生定义
* 所有需要在 C++ 代码中直接引用的 Tag 都通过此文件定义
* 获得编译期类型检查和 IDE 自动补全
*/
namespace XiYouGameplayTags
{
// 无效 Tag(默认值)
XIYOU_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(None);
// ============== Input Tags(输入映射) ==============
// WASD 移动输入,映射到 IA_Move
XIYOU_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_Move);
// 左键点击移动输入,映射到 IA_ClickMove
XIYOU_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_ClickMove);
}
源文件设计
cpp
// XiYouGameplayTags.cpp
#include "Core/XiYouGameplayTags.h"
namespace XiYouGameplayTags
{
// 使用 UE_DEFINE_GAMEPLAY_TAG_COMMENT 而非 UE_DEFINE_GAMEPLAY_TAG
// 第三个参数是标签说明注释,会显示在 UE 编辑器的 Gameplay Tags 管理器中
UE_DEFINE_GAMEPLAY_TAG_COMMENT(None, "None", "无效Tag");
// ============== Input Tags ==============
UE_DEFINE_GAMEPLAY_TAG_COMMENT(InputTag_Move, "InputTag.Move", "WASD 移动输入");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(InputTag_ClickMove, "InputTag.ClickMove", "左键点击移动输入");
}
使用方式
在代码中引用 Tag 时,使用命名空间变量而非硬编码字符串:
cpp
// ❌ 不推荐:硬编码字符串,容易拼写错误,无编译期检查
FGameplayTag::RequestGameplayTag(FName("InputTag.Move"));
// ✅ 推荐:使用 C++ 原生定义的命名空间变量,有编译期类型检查和 IDE 自动补全
XiYouGameplayTags::InputTag_Move
与 InputConfig 的配合
在 UXiYouInputConfig 的 DataAsset 配置中,InputTag 字段已有 meta=(Categories="InputTag") 约束,确保蓝图只能选择 InputTag 分类下的 Tag。C++ 定义后,这些 Tag 会自动出现在编辑器的 Tag 选择器中,无需手动在 INI 中注册。
与 PlayerController 的配合
在 AXiYouPlayerController 的 SetupInputComponent() 中,绑定 Tag 时使用 C++ 原生定义的命名空间变量:
cpp
// 从 InputConfig 获取 InputAction 和对应的 InputTag
// 使用 C++ 原生定义的 Tag 变量
UXiYouInputComponent* XiYouInputComp = Cast<UXiYouInputComponent>(InputComponent);
XiYouInputComp->BindNativeAction(InputConfig, XiYouGameplayTags::InputTag_Move, ETriggerEvent::Triggered, this, &AXiYouPlayerController::HandleMove);
XiYouInputComp->BindNativeAction(InputConfig, XiYouGameplayTags::InputTag_ClickMove, ETriggerEvent::Triggered, this, &AXiYouPlayerController::HandleClickMove);
后续扩展规划
T03 阶段仅定义 InputTag 类别。后续任务将逐步扩展:
| 任务 | 新增 Tag 类别 | 示例 |
|---|---|---|
| T04 GAS 集成 | Identity(阵营标识) | Identity.Character.Hero、Identity.Enemy |
| T05 属性系统 | Status(状态同步) | Shared.Status.Dead、Player.Status.Attacking |
| T07 技能释放 | Ability、Event、Cooldown | Player.Ability.Attack.Light、Shared.Event.MeleeHit |
| T08 Buff/Debuff | Status(控制效果) | State.Stun、State.Silence、State.Root |
三、移动系统设计
3.1 移动方式总览
| 移动方式 | 平台 | 实现状态 | 输入驱动方式 |
|---|---|---|---|
| WASD/方向键移动 | PC | ✅ T03 实现 | EnhancedInput + InputTag.Move |
| 左键点击地面移动 | PC | ✅ T03 实现 | EnhancedInput + InputTag.ClickMove |
| 触摸点击地面移动 | Mobile | ✅ T03 实现(与 PC 端行为一致) | EnhancedInput + InputTag.ClickMove(T10 在 IMC_Mobile 映射触摸) |
| 触摸点击敌人移动到攻击射程 | Mobile | ⏳ T07 扩展 | T03 预留目标 Actor 参数,T07 实现追踪+攻击 |
| 虚拟摇杆 | Mobile | ⏳ T10 实现 | T03 不做,留接口 |
3.2 WASD 移动设计(世界坐标方向)
设计决策 #6:世界坐标方向
决策说明 :WASD 移动基于 世界坐标方向,而非摄像机方向或角色自身朝向。
- W = +X(世界前方)
- S = -X(世界后方)
- A = -Y(世界左方)
- D = +Y(世界右方)
理由 :俯视角摄像机角度固定(55°),不可旋转,因此世界坐标方向与玩家视觉方向一致,无需依赖摄像机旋转计算。角色通过
bOrientRotationToMovement = true自动转向移动方向。
InputAction 资产
需要在 Content 目录中创建以下 EnhancedInput 资产(蓝图资产,详见蓝图操作指南):
| 资产名 | 类型 | Value Type | 说明 |
|---|---|---|---|
IA_Move |
UInputAction |
Axis2D (Vector2D) |
WASD 移动输入 |
IA_ClickMove |
UInputAction |
Digital (bool) |
左键点击移动 |
IMC_PC 映射配置
| InputAction | 键位 | 说明 |
|---|---|---|
IA_Move |
W | Forward (+Y in 2D → +X in world) |
IA_Move |
S | Backward (-Y in 2D → -X in world) |
IA_Move |
A | Left (-X in 2D → -Y in world) |
IA_Move |
D | Right (+X in 2D → +Y in world) |
注意 :EnhancedInput 的 Axis2D 中 X = 水平(A/D),Y = 垂直(W/S)。在 HandleMove 中映射到世界坐标时:
MoveVector.Y→ Forward(+X),MoveVector.X→ Right(+Y)。
GameplayTag 定义(C++ 原生定义模式)
设计决策 :采用 C++ 原生定义模式 (
UE_DECLARE_GAMEPLAY_TAG_EXTERN+UE_DEFINE_GAMEPLAY_TAG_COMMENT),而非纯 INI 注册。所有需要在 C++ 代码中直接引用的 Tag 都通过XiYouGameplayTags.h/XiYouGameplayTags.cpp定义,获得编译期类型检查和 IDE 自动补全。详见 §2.7。
T03 阶段定义的 GameplayTag:
| Tag 字符串 | C++ 变量 | 说明 |
|---|---|---|
InputTag.Move |
XiYouGameplayTags::InputTag_Move |
WASD 移动输入,映射到 IA_Move |
InputTag.ClickMove |
XiYouGameplayTags::InputTag_ClickMove |
左键点击移动输入,映射到 IA_ClickMove |
INI 配置改为补充性质:
Config/DefaultGameplayTags.ini 中不再需要手动注册 InputTag(C++ 已定义),但保留用于 GameplayCue 等不需要 C++ 直接引用的辅助 Tag:
ini
[/Script/GameplayTags.GameplayTagsSettings]
; InputTag 已通过 C++ 原生定义(XiYouGameplayTags.h/.cpp),无需在此重复注册
; 此文件用于补充 GameplayCue 等不需要 C++ 直接引用的辅助 Tag
使用方式:
cpp
// ❌ 不推荐:硬编码字符串
FGameplayTag::RequestGameplayTag(FName("InputTag.Move"));
// ✅ 推荐:使用 C++ 原生定义的命名空间变量
XiYouGameplayTags::InputTag_Move
移动逻辑流程
#mermaid-svg-QwvigOPo1mF4mBYl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QwvigOPo1mF4mBYl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QwvigOPo1mF4mBYl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QwvigOPo1mF4mBYl .error-icon{fill:#552222;}#mermaid-svg-QwvigOPo1mF4mBYl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QwvigOPo1mF4mBYl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QwvigOPo1mF4mBYl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QwvigOPo1mF4mBYl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QwvigOPo1mF4mBYl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QwvigOPo1mF4mBYl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QwvigOPo1mF4mBYl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QwvigOPo1mF4mBYl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QwvigOPo1mF4mBYl .marker.cross{stroke:#333333;}#mermaid-svg-QwvigOPo1mF4mBYl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QwvigOPo1mF4mBYl p{margin:0;}#mermaid-svg-QwvigOPo1mF4mBYl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QwvigOPo1mF4mBYl .cluster-label text{fill:#333;}#mermaid-svg-QwvigOPo1mF4mBYl .cluster-label span{color:#333;}#mermaid-svg-QwvigOPo1mF4mBYl .cluster-label span p{background-color:transparent;}#mermaid-svg-QwvigOPo1mF4mBYl .label text,#mermaid-svg-QwvigOPo1mF4mBYl span{fill:#333;color:#333;}#mermaid-svg-QwvigOPo1mF4mBYl .node rect,#mermaid-svg-QwvigOPo1mF4mBYl .node circle,#mermaid-svg-QwvigOPo1mF4mBYl .node ellipse,#mermaid-svg-QwvigOPo1mF4mBYl .node polygon,#mermaid-svg-QwvigOPo1mF4mBYl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QwvigOPo1mF4mBYl .rough-node .label text,#mermaid-svg-QwvigOPo1mF4mBYl .node .label text,#mermaid-svg-QwvigOPo1mF4mBYl .image-shape .label,#mermaid-svg-QwvigOPo1mF4mBYl .icon-shape .label{text-anchor:middle;}#mermaid-svg-QwvigOPo1mF4mBYl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QwvigOPo1mF4mBYl .rough-node .label,#mermaid-svg-QwvigOPo1mF4mBYl .node .label,#mermaid-svg-QwvigOPo1mF4mBYl .image-shape .label,#mermaid-svg-QwvigOPo1mF4mBYl .icon-shape .label{text-align:center;}#mermaid-svg-QwvigOPo1mF4mBYl .node.clickable{cursor:pointer;}#mermaid-svg-QwvigOPo1mF4mBYl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QwvigOPo1mF4mBYl .arrowheadPath{fill:#333333;}#mermaid-svg-QwvigOPo1mF4mBYl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QwvigOPo1mF4mBYl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QwvigOPo1mF4mBYl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QwvigOPo1mF4mBYl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QwvigOPo1mF4mBYl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QwvigOPo1mF4mBYl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QwvigOPo1mF4mBYl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QwvigOPo1mF4mBYl .cluster text{fill:#333;}#mermaid-svg-QwvigOPo1mF4mBYl .cluster span{color:#333;}#mermaid-svg-QwvigOPo1mF4mBYl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QwvigOPo1mF4mBYl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QwvigOPo1mF4mBYl rect.text{fill:none;stroke-width:0;}#mermaid-svg-QwvigOPo1mF4mBYl .icon-shape,#mermaid-svg-QwvigOPo1mF4mBYl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QwvigOPo1mF4mBYl .icon-shape p,#mermaid-svg-QwvigOPo1mF4mBYl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QwvigOPo1mF4mBYl .icon-shape .label rect,#mermaid-svg-QwvigOPo1mF4mBYl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QwvigOPo1mF4mBYl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QwvigOPo1mF4mBYl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QwvigOPo1mF4mBYl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} WASD 按下
IA_Move Triggered
PlayerController HandleMove
停止点击移动
世界坐标方向计算
AddMovementInput
CharacterMovementComponent 处理
客户端预测移动
服务端校正
关键代码(PlayerController 中实现)
cpp
void AXiYouPlayerController::HandleMove(const FInputActionValue& Value)
{
if (!GetPawn())
{
return;
}
// 获取 2D 输入向量
const FVector2D MoveVector = Value.Get<FVector2D>();
// 停止点击移动(WASD 优先于点击移动)
if (AXiYouPlayerCharacter* PlayerChar = Cast<AXiYouPlayerCharacter>(GetPawn()))
{
PlayerChar->StopClickMove();
}
// 设计决策 #6:世界坐标方向(俯视角固定摄像机)
const FVector ForwardDirection = FVector::ForwardVector; // +X
const FVector RightDirection = FVector::RightVector; // +Y
// 应用移动输入
GetPawn()->AddMovementInput(ForwardDirection, MoveVector.Y);
GetPawn()->AddMovementInput(RightDirection, MoveVector.X);
}
3.3 左键点击地面移动设计
设计决策 #1:左键移动
决策说明 :点击移动使用 左键,与 PRD 中"右键点击移动"的要求有意偏差。
理由:
- ARPG 游戏惯例(如 Diablo、Lost Ark、Path of Exile)均使用左键点击移动
- 右键通常预留用于技能释放或目标选择
- 左键移动 + 右键技能是玩家肌肉记忆,降低学习成本
- PRD 的右键要求可在后续根据用户反馈调整,当前以游戏惯例为准
设计决策 #7:SimpleMoveToLocation + 边界场景处理
决策说明 :使用
UNavigationSystemV1::SimpleMoveToLocation进行寻路,并特别处理以下不可达边界场景:
- 点击 NavMesh 外区域(如墙壁内部、空中)
- 点击不可达位置(被障碍物完全包围)
- 点击距离过远(防作弊/防异常)
设计决策 #8:客户端本地预测 + 服务端校正
决策说明 :客户端点击后 立即本地 SimpleMoveToLocation (低延迟响应),同时发送 Server RPC 验证。服务端验证通过后执行权威移动,通过 ReplicatedMovement 同步回客户端。
流程:
- 客户端点击 → 本地 NavMesh 检查 → 本地 SimpleMoveToLocation(即时响应)
- 同时发送 ServerMoveToTarget RPC(Reliable)
- 服务端验证 → 服务端 SimpleMoveToLocation(权威移动)
- ReplicatedMovement 同步 → 客户端平滑插值
交互规则
| 操作 | 行为 |
|---|---|
| 左键点击地面 | 角色寻路移动到点击位置 |
| 移动中再次左键点击 | 改变目标点,角色转向新目标 |
| WASD 按下 | 取消点击移动,切换为手动移动 |
| 右键点击 | 不参与移动(预留技能/目标选择) |
手机端适配说明
核心原则 :手机端点击地面移动与 PC 端左键点击地面移动行为完全一致 ,共用同一个
IA_ClickMoveInputAction。
手机端点击移动行为:
| 操作 | 行为 | 实现阶段 |
|---|---|---|
| 手机端触摸点击地面 | 角色寻路移动到触摸位置(与 PC 端左键点击地面行为一致) | ✅ T03 实现 |
| 手机端触摸点击敌人 | 角色移动到敌人附近(进入攻击射程后停止移动,由 T07 技能释放系统接管) | ⏳ T07 扩展 |
实现机制:
-
InputAction 共用 :手机端触摸事件通过 EnhancedInput 的
IA_ClickMove触发,与 PC 端左键点击共用同一个 InputAction。T03 阶段IA_ClickMove在IMC_PC中映射到鼠标左键。 -
T10 平台映射 :T10(手机端输入适配)会在
IMC_Mobile中将触摸手势映射到IA_ClickMove。运行时根据平台自动切换IMC_PC/IMC_Mobile(详见 §10.4 T10 预留接口)。 -
T03 阶段范围:T03 只实现"点击地面 → 移动到目标位置"的基础逻辑。"点击敌人 → 移动到攻击射程 → 自动攻击"的完整逻辑由 T07(技能释放与战斗逻辑)实现。
-
目标 Actor 参数预留 :T03 的点击移动系统需要预留目标 Actor 参数(而非仅
FVector目标位置),以便 T07 扩展为"追踪目标移动"。详见下方ServerMoveToTargetRPC 扩展说明和 §10.6 T07 预留接口。
点击移动流程图
Dedicated Server PlayerCharacter NavigationSystem PlayerController 玩家 Dedicated Server PlayerCharacter NavigationSystem PlayerController 玩家 #mermaid-svg-uwE5al7OogiloWBp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uwE5al7OogiloWBp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uwE5al7OogiloWBp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uwE5al7OogiloWBp .error-icon{fill:#552222;}#mermaid-svg-uwE5al7OogiloWBp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uwE5al7OogiloWBp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uwE5al7OogiloWBp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uwE5al7OogiloWBp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uwE5al7OogiloWBp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uwE5al7OogiloWBp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uwE5al7OogiloWBp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uwE5al7OogiloWBp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uwE5al7OogiloWBp .marker.cross{stroke:#333333;}#mermaid-svg-uwE5al7OogiloWBp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uwE5al7OogiloWBp p{margin:0;}#mermaid-svg-uwE5al7OogiloWBp .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uwE5al7OogiloWBp text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-uwE5al7OogiloWBp .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uwE5al7OogiloWBp .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-uwE5al7OogiloWBp .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-uwE5al7OogiloWBp .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-uwE5al7OogiloWBp #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-uwE5al7OogiloWBp .sequenceNumber{fill:white;}#mermaid-svg-uwE5al7OogiloWBp #sequencenumber{fill:#333;}#mermaid-svg-uwE5al7OogiloWBp #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-uwE5al7OogiloWBp .messageText{fill:#333;stroke:none;}#mermaid-svg-uwE5al7OogiloWBp .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uwE5al7OogiloWBp .labelText,#mermaid-svg-uwE5al7OogiloWBp .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-uwE5al7OogiloWBp .loopText,#mermaid-svg-uwE5al7OogiloWBp .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-uwE5al7OogiloWBp .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uwE5al7OogiloWBp .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-uwE5al7OogiloWBp .noteText,#mermaid-svg-uwE5al7OogiloWBp .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-uwE5al7OogiloWBp .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uwE5al7OogiloWBp .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uwE5al7OogiloWBp .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uwE5al7OogiloWBp .actorPopupMenu{position:absolute;}#mermaid-svg-uwE5al7OogiloWBp .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-uwE5al7OogiloWBp .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uwE5al7OogiloWBp .actor-man circle,#mermaid-svg-uwE5al7OogiloWBp line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-uwE5al7OogiloWBp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端平滑插值 alt 不可达 可达 左键点击地面 GetHitResultUnderCursor 检测 ProjectPointToNavigation 投影 返回 NavLocation IsLocationReachable 可达性检查 返回可达/不可达 忽略点击 本地 MoveToLocation(客户端预测) ServerMoveToTarget(Location) Reliable 验证距离/合法性 服务端 MoveToLocation(权威移动) SimpleMoveToLocation 返回寻路路径 CMC 沿路径移动 ReplicatedMovement 同步
PlayerController 中的点击移动处理
cpp
// XiYouPlayerController.h - T03 新增部分
protected:
/** WASD 移动回调(InputTag.Move 驱动) */
void HandleMove(const FInputActionValue& Value);
/** 左键点击移动回调(InputTag.ClickMove 驱动) */
void HandleClickMove();
public:
/** Server RPC - 请求移动到目标位置(服务端验证+权威移动) */
UFUNCTION(Server, Reliable, WithValidation)
void ServerMoveToTarget(FVector TargetLocation);
// ===== T07 扩展预留说明 =====
// 当前 T03 的 ServerMoveToTarget 只接受 FVector 目标位置参数(点击地面移动)。
// T07(技能释放与战斗逻辑)扩展时可增加 AActor* 目标 Actor 参数,
// 实现"追踪目标移动"(角色持续向目标 Actor 移动,进入攻击射程后停止):
//
// UFUNCTION(Server, Reliable, WithValidation)
// void ServerMoveToActor(AActor* TargetActor);
//
// 点击敌人时,T07 会判断目标类型(地面 vs 敌人),
// 分别走 ServerMoveToTarget(移动到位置)或 ServerMoveToActor(追踪+攻击)。
// 详见 §10.6 T07 预留接口。
private:
/** 输入配置资产引用(从 GameInstance 或编辑器设置获取) */
UPROPERTY(EditAnywhere, Category = "Input")
UXiYouInputConfig* InputConfig;
cpp
// XiYouPlayerController.cpp - T03 新增部分
#include "Core/XiYouInputConfig.h"
#include "Core/XiYouInputComponent.h"
#include "Character/XiYouPlayerCharacter.h"
#include "EnhancedInputSubsystems.h"
#include "NavigationSystem.h"
#include "InputActionValue.h"
void AXiYouPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
// 获取自定义 InputComponent
UXiYouInputComponent* XiYouInput = Cast<UXiYouInputComponent>(InputComponent);
if (!XiYouInput || !InputConfig)
{
return;
}
// 绑定 Native 输入动作(Tag 驱动,使用 C++ 原生定义的命名空间变量)
XiYouInput->BindNativeAction(InputConfig, XiYouGameplayTags::InputTag_Move,
ETriggerEvent::Triggered, this, &AXiYouPlayerController::HandleMove);
XiYouInput->BindNativeAction(InputConfig, XiYouGameplayTags::InputTag_ClickMove,
ETriggerEvent::Triggered, this, &AXiYouPlayerController::HandleClickMove);
// 保留 T02 鼠标滚轮轴绑定(旧式轴映射,EnhancedInput 不管理轴)
InputComponent->BindAxis("MouseWheel", this, &AXiYouPlayerController::OnMouseWheel);
}
void AXiYouPlayerController::HandleMove(const FInputActionValue& Value)
{
if (!GetPawn())
{
return;
}
const FVector2D MoveVector = Value.Get<FVector2D>();
// 停止点击移动(WASD 优先)
if (AXiYouPlayerCharacter* PlayerChar = Cast<AXiYouPlayerCharacter>(GetPawn()))
{
PlayerChar->StopClickMove();
}
// 设计决策 #6:世界坐标方向
const FVector ForwardDirection = FVector::ForwardVector; // +X
const FVector RightDirection = FVector::RightVector; // +Y
GetPawn()->AddMovementInput(ForwardDirection, MoveVector.Y);
GetPawn()->AddMovementInput(RightDirection, MoveVector.X);
}
void AXiYouPlayerController::HandleClickMove()
{
if (!GetPawn())
{
return;
}
// 鼠标射线检测
FHitResult HitResult;
GetHitResultUnderCursor(ECC_Visibility, true, HitResult);
if (!HitResult.bBlockingHit)
{
return; // 未命中任何物体
}
FVector TargetLocation = HitResult.Location;
// 边界检查 1:NavMesh 可达性
if (UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld()))
{
// 检查目标点是否在 NavMesh 上(投影到最近导航点)
FNavLocation NavLocation;
if (!NavSys->ProjectPointToNavigation(TargetLocation, NavLocation, FVector(500, 500, 500)))
{
return; // 目标点不在 NavMesh 上,忽略点击
}
TargetLocation = NavLocation.Location;
// 检查路径可达性
if (!NavSys->IsLocationReachable(GetPawn()->GetActorLocation(), TargetLocation))
{
return; // 不可达,忽略点击
}
}
// 边界检查 2:距离限制(防异常)
float Distance = FVector::Dist(GetPawn()->GetActorLocation(), TargetLocation);
if (Distance > 10000.0f)
{
return; // 距离过远,忽略
}
// 设计决策 #8:客户端本地预测移动(低延迟响应)
if (AXiYouPlayerCharacter* PlayerChar = Cast<AXiYouPlayerCharacter>(GetPawn()))
{
PlayerChar->MoveToLocation(TargetLocation);
}
// 同时发送到服务端验证
ServerMoveToTarget(TargetLocation);
}
void AXiYouPlayerController::ServerMoveToTarget_Implementation(FVector TargetLocation)
{
// 服务端验证:检查目标位置合法性
if (!GetPawn())
{
return;
}
// 距离验证(防作弊)
float Distance = FVector::Dist(GetPawn()->GetActorLocation(), TargetLocation);
if (Distance > 10000.0f)
{
return; // 距离过远,拒绝
}
// NavMesh 可达性验证
if (UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld()))
{
FNavLocation NavLocation;
if (!NavSys->ProjectPointToNavigation(TargetLocation, NavLocation, FVector(500, 500, 500)))
{
return; // 目标不在 NavMesh 上,拒绝
}
TargetLocation = NavLocation.Location;
}
// 服务端权威移动
if (AXiYouPlayerCharacter* PlayerChar = Cast<AXiYouPlayerCharacter>(GetPawn()))
{
PlayerChar->MoveToLocation(TargetLocation);
}
}
bool AXiYouPlayerController::ServerMoveToTarget_Validate(FVector TargetLocation)
{
// 基础验证:位置不能太远
if (GetPawn())
{
float Distance = FVector::Dist2D(GetPawn()->GetActorLocation(), TargetLocation);
if (Distance > 10000.0f)
{
return false;
}
}
return true;
}
3.4 俯视角移动配置参数
| 参数 | 值 | 说明 |
|---|---|---|
bUseControllerRotationYaw |
false |
不使用控制器旋转 Yaw |
bUseControllerRotationPitch |
false |
不使用控制器旋转 Pitch |
bUseControllerRotationRoll |
false |
不使用控制器旋转 Roll |
bOrientRotationToMovement |
true |
角色朝向移动方向 |
bUseControllerDesiredRotation |
false |
不使用控制器期望旋转 |
RotationRate |
(0, 720, 0) |
旋转速度 720°/秒 |
MovementMode |
MOVE_Walking |
行走模式 |
NetworkSmoothingMode |
Exponential |
指数平滑(减少网络抖动) |
NetworkSimulatedSmoothLocationTime |
0.1 |
位置平滑时间 |
NetworkSimulatedSmoothRotationTime |
0.1 |
旋转平滑时间 |
JumpZVelocity |
0.0 |
禁用跳跃 |
AirControl |
0.0 |
无空中控制 |
3.5 CharacterMovementComponent 关键参数
| 参数 | 默认值 | 说明 |
|---|---|---|
MaxWalkSpeed |
600 |
最大行走速度(可在子类/蓝图中调整) |
MaxAcceleration |
2048 |
最大加速度 |
BrakingDecelerationWalking |
2048 |
行走减速度 |
BrakingFrictionFactor |
2.0 |
制动摩擦系数 |
bUseFixedBrakingDistance |
true |
使用固定制动距离 |
GroundFriction |
8.0 |
地面摩擦力 |
四、动画系统设计
4.1 动画状态机设计
设计决策 #3 落地 :T03 只实现 Idle/Walk/Run 三个状态,不实现 Death 动画。Death 动画留到 T05(属性系统与伤害计算)/T07(技能释放与战斗逻辑)实现。
#mermaid-svg-FPuTIa5Yr0qf5IM3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .error-icon{fill:#552222;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .marker.cross{stroke:#333333;}#mermaid-svg-FPuTIa5Yr0qf5IM3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 p{margin:0;}#mermaid-svg-FPuTIa5Yr0qf5IM3 defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-FPuTIa5Yr0qf5IM3 g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-FPuTIa5Yr0qf5IM3 g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-FPuTIa5Yr0qf5IM3 g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FPuTIa5Yr0qf5IM3 .edgeLabel .label text{fill:#333;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .label div .edgeLabel{color:#333;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 #statediagram-barbEnd{fill:#333333;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .cluster-label,#mermaid-svg-FPuTIa5Yr0qf5IM3 .nodeLabel{color:#131300;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .note-edge{stroke-dasharray:5;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-note text{fill:black;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram-note .nodeLabel{color:black;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagram .edgeLabel{color:red;}#mermaid-svg-FPuTIa5Yr0qf5IM3 #dependencyStart,#mermaid-svg-FPuTIa5Yr0qf5IM3 #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-FPuTIa5Yr0qf5IM3 .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FPuTIa5Yr0qf5IM3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Speed > 10 && Speed <= 200
Speed > 200
Speed <= 10
Speed > 200
Speed <= 200 && Speed > 10
Speed <= 10
Idle
Walk
Run
4.2 状态节点说明
| 状态 | 速度范围 | 动画资产 |
|---|---|---|
| Idle | Speed <= 10 |
Idle 动画(呼吸待机) |
| Walk | 10 < Speed <= 200 |
Walk 动画(慢走) |
| Run | Speed > 200 |
Run 动画(快跑) |
4.3 速度驱动逻辑
动画状态机基于角色的 实际移动速度 (GetVelocity().Length())驱动状态切换:
cpp
// 在 AnimBP 的 Event Blueprint Update Animation 中获取速度
float Speed = 0.0f;
if (APawn* Pawn = TryGetPawnOwner())
{
Speed = Pawn->GetVelocity().Length();
}
4.4 动画资产来源
设计决策 #9 落地:使用 Lyra 项目或其他 Epic 免费示例项目的动画资源(如 ALS、Mannequin)。
| 项目 | 说明 |
|---|---|
| 动画资产来源 | Lyra 示例项目 / Fab 免费动画资产(Mannequin 骨骼) |
| 引用方式 | 在 AnimBP 中通过 Play Anim Sequence 节点引用 UAnimSequence |
| 骨骼 | UE5 Mannequin(与 Lyra/ALS 兼容) |
| AnimBP 路径 | Content/Animations/ABP_XiYouCharacter |
4.5 AnimBP 创建步骤
- 获取 Lyra 或 Fab 免费动画资产(Idle/Walk/Run)
- 在 UE 编辑器中创建 AnimBP:基于 UE5 Mannequin 骨骼
- 创建状态机:
Idle→Walk→Run三个状态 - 每个状态使用
Play Anim Sequence节点播放对应动画 - 转换条件基于
Speed变量(Event Blueprint Update Animation 中更新) - 在角色蓝图
BP_XiYouPlayerCharacter中设置AnimClass为此 AnimBP 类
详细操作步骤 :见
plans/P0-T03-Blueprint-Guide.md
4.6 动画蓝图与 C++ 的关系
#mermaid-svg-ZkHrs0rpjQvjTLmx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZkHrs0rpjQvjTLmx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZkHrs0rpjQvjTLmx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZkHrs0rpjQvjTLmx .error-icon{fill:#552222;}#mermaid-svg-ZkHrs0rpjQvjTLmx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZkHrs0rpjQvjTLmx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZkHrs0rpjQvjTLmx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZkHrs0rpjQvjTLmx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZkHrs0rpjQvjTLmx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZkHrs0rpjQvjTLmx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZkHrs0rpjQvjTLmx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZkHrs0rpjQvjTLmx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZkHrs0rpjQvjTLmx .marker.cross{stroke:#333333;}#mermaid-svg-ZkHrs0rpjQvjTLmx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZkHrs0rpjQvjTLmx p{margin:0;}#mermaid-svg-ZkHrs0rpjQvjTLmx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZkHrs0rpjQvjTLmx .cluster-label text{fill:#333;}#mermaid-svg-ZkHrs0rpjQvjTLmx .cluster-label span{color:#333;}#mermaid-svg-ZkHrs0rpjQvjTLmx .cluster-label span p{background-color:transparent;}#mermaid-svg-ZkHrs0rpjQvjTLmx .label text,#mermaid-svg-ZkHrs0rpjQvjTLmx span{fill:#333;color:#333;}#mermaid-svg-ZkHrs0rpjQvjTLmx .node rect,#mermaid-svg-ZkHrs0rpjQvjTLmx .node circle,#mermaid-svg-ZkHrs0rpjQvjTLmx .node ellipse,#mermaid-svg-ZkHrs0rpjQvjTLmx .node polygon,#mermaid-svg-ZkHrs0rpjQvjTLmx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZkHrs0rpjQvjTLmx .rough-node .label text,#mermaid-svg-ZkHrs0rpjQvjTLmx .node .label text,#mermaid-svg-ZkHrs0rpjQvjTLmx .image-shape .label,#mermaid-svg-ZkHrs0rpjQvjTLmx .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZkHrs0rpjQvjTLmx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZkHrs0rpjQvjTLmx .rough-node .label,#mermaid-svg-ZkHrs0rpjQvjTLmx .node .label,#mermaid-svg-ZkHrs0rpjQvjTLmx .image-shape .label,#mermaid-svg-ZkHrs0rpjQvjTLmx .icon-shape .label{text-align:center;}#mermaid-svg-ZkHrs0rpjQvjTLmx .node.clickable{cursor:pointer;}#mermaid-svg-ZkHrs0rpjQvjTLmx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZkHrs0rpjQvjTLmx .arrowheadPath{fill:#333333;}#mermaid-svg-ZkHrs0rpjQvjTLmx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZkHrs0rpjQvjTLmx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZkHrs0rpjQvjTLmx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZkHrs0rpjQvjTLmx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZkHrs0rpjQvjTLmx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZkHrs0rpjQvjTLmx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZkHrs0rpjQvjTLmx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZkHrs0rpjQvjTLmx .cluster text{fill:#333;}#mermaid-svg-ZkHrs0rpjQvjTLmx .cluster span{color:#333;}#mermaid-svg-ZkHrs0rpjQvjTLmx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZkHrs0rpjQvjTLmx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZkHrs0rpjQvjTLmx rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZkHrs0rpjQvjTLmx .icon-shape,#mermaid-svg-ZkHrs0rpjQvjTLmx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZkHrs0rpjQvjTLmx .icon-shape p,#mermaid-svg-ZkHrs0rpjQvjTLmx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZkHrs0rpjQvjTLmx .icon-shape .label rect,#mermaid-svg-ZkHrs0rpjQvjTLmx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZkHrs0rpjQvjTLmx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZkHrs0rpjQvjTLmx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZkHrs0rpjQvjTLmx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AnimClass 属性
Event Update
Speed 变量
播放
播放
播放
C++ AXiYouCharacter
AnimBP 资产
获取 Pawn 速度
状态机
Idle 状态
Walk 状态
Run 状态
Idle AnimSequence
Walk AnimSequence
Run AnimSequence
五、网络同步设计
5.1 CharacterMovementComponent 自带同步说明
UE5 的 UCharacterMovementComponent 内置完整的网络同步机制,T03 不自定义任何移动同步逻辑:
| 机制 | 说明 |
|---|---|
| 客户端预测 | 客户端本地模拟移动,不等服务端确认即更新位置 |
| 服务端校正 | 服务端验证移动合法性,如有偏差发送校正 |
| Replication | 角色的 Location/Rotation/Velocity 自动复制 |
| Network Smoothing | 模拟代理(非控制端)使用平滑插值减少抖动 |
5.2 角色复制配置
在 AXiYouCharacter 构造函数中设置复制属性:
cpp
// 角色需要网络复制
bReplicates = true;
// 复制模式:最小化带宽(只复制给相关客户端)
bOnlyRelevantToOwner = false;
bAlwaysRelevant = false;
bNetLoadOnClient = true;
bReplicateMovement = true; // 自动复制 Movement
5.3 点击移动的 RPC 流程(客户端预测 + 服务端校正)
设计决策 #8 落地:客户端本地预测移动 + 服务端校正
Server Client Server Client #mermaid-svg-k0qzqRU7KL6oa2q7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-k0qzqRU7KL6oa2q7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-k0qzqRU7KL6oa2q7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-k0qzqRU7KL6oa2q7 .error-icon{fill:#552222;}#mermaid-svg-k0qzqRU7KL6oa2q7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-k0qzqRU7KL6oa2q7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-k0qzqRU7KL6oa2q7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-k0qzqRU7KL6oa2q7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-k0qzqRU7KL6oa2q7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-k0qzqRU7KL6oa2q7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-k0qzqRU7KL6oa2q7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-k0qzqRU7KL6oa2q7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-k0qzqRU7KL6oa2q7 .marker.cross{stroke:#333333;}#mermaid-svg-k0qzqRU7KL6oa2q7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-k0qzqRU7KL6oa2q7 p{margin:0;}#mermaid-svg-k0qzqRU7KL6oa2q7 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-k0qzqRU7KL6oa2q7 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-k0qzqRU7KL6oa2q7 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-k0qzqRU7KL6oa2q7 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-k0qzqRU7KL6oa2q7 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-k0qzqRU7KL6oa2q7 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-k0qzqRU7KL6oa2q7 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-k0qzqRU7KL6oa2q7 .sequenceNumber{fill:white;}#mermaid-svg-k0qzqRU7KL6oa2q7 #sequencenumber{fill:#333;}#mermaid-svg-k0qzqRU7KL6oa2q7 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-k0qzqRU7KL6oa2q7 .messageText{fill:#333;stroke:none;}#mermaid-svg-k0qzqRU7KL6oa2q7 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-k0qzqRU7KL6oa2q7 .labelText,#mermaid-svg-k0qzqRU7KL6oa2q7 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-k0qzqRU7KL6oa2q7 .loopText,#mermaid-svg-k0qzqRU7KL6oa2q7 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-k0qzqRU7KL6oa2q7 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-k0qzqRU7KL6oa2q7 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-k0qzqRU7KL6oa2q7 .noteText,#mermaid-svg-k0qzqRU7KL6oa2q7 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-k0qzqRU7KL6oa2q7 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-k0qzqRU7KL6oa2q7 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-k0qzqRU7KL6oa2q7 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-k0qzqRU7KL6oa2q7 .actorPopupMenu{position:absolute;}#mermaid-svg-k0qzqRU7KL6oa2q7 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-k0qzqRU7KL6oa2q7 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-k0qzqRU7KL6oa2q7 .actor-man circle,#mermaid-svg-k0qzqRU7KL6oa2q7 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-k0qzqRU7KL6oa2q7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 玩家左键点击地面 服务端权威移动 客户端预测 + 平滑插值 GetHitResultUnderCursor NavMesh 可达性检查 本地 MoveToLocation(客户端预测,即时响应) ServerMoveToTarget(Location) Reliable Validate - 距离检查 NavMesh 可达性验证 MoveToLocation - NavSys.SimpleMoveToLocation CharacterMovementComponent 沿路径移动 ReplicatedMovement 同步
5.4 DefaultEngine.ini 网络配置补充
设计决策 #9 落地:补充 CMC 网络平滑配置和 NetworkManager 配置。
在 Config/DefaultEngine.ini 中添加以下配置段:
ini
[/Script/Engine.CharacterMovementComponent]
NetworkSimulatedSmoothLocationTime=0.1
NetworkSimulatedSmoothRotationTime=0.1
[/Script/Engine.NetworkManager]
NetServerMaxTickRate=30
MaxClientUpdateRate=30
MaxNetTickRate=30
5.5 角色属性复制列表
T03 阶段角色基类不定义自定义复制属性 (Health/Mana 等留到 T04/T05 通过 AttributeSet/GAS 管理)。仅使用 UE5 内置的 ReplicatedMovement 结构体自动复制:
| 属性 | 复制方式 | 说明 |
|---|---|---|
Location |
bReplicateMovement |
自动复制位置 |
Rotation |
bReplicateMovement |
自动复制旋转 |
Velocity |
bReplicateMovement |
自动复制速度 |
bIsDead |
--- | T05 实现(通过 AttributeSet) |
六、PlayerController 集成
6.1 现有代码(T01/T02 已有)
bShowMouseCursor = true/bEnableClickEvents = true/bEnableTouchEvents = trueCameraActor引用(UPROPERTY(VisibleAnywhere))SpawnCamera()--- 生成摄像机并设置 ViewTargetOnPossess()/BeginPlay()--- 调用 SpawnCamera()SetupInputComponent()--- 绑定 "MouseWheel" 轴OnMouseWheel()--- 鼠标滚轮缩放回调
6.2 T03 新增内容
头文件完整修改后
cpp
// XiYouPlayerController.h - T03 修改后完整内容
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "XiYouPlayerController.generated.h"
// 前向声明
class AXiYouCameraActor;
class UXiYouInputConfig;
struct FInputActionValue;
/**
* XiYouPlayerController - 输入处理和RPC入口
* 负责玩家输入处理(Tag 驱动)、摄像机管理、RPC通信
*/
UCLASS()
class XIYOU_API AXiYouPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AXiYouPlayerController();
// ===== 摄像机相关 =====
/** 摄像机Actor引用 */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
AXiYouCameraActor* CameraActor;
/** 生成摄像机并设置ViewTarget(仅客户端调用) */
UFUNCTION(BlueprintCallable, Category = "Camera")
void SpawnCamera();
protected:
// ===== 生命周期 =====
virtual void OnPossess(APawn* InPawn) override;
virtual void BeginPlay() override;
// ===== 输入处理 =====
virtual void SetupInputComponent() override;
/** 鼠标滚轮缩放回调 */
void OnMouseWheel(float AxisValue);
// ===== T03 新增:移动输入处理 =====
/** WASD 移动回调(InputTag.Move 驱动) */
void HandleMove(const FInputActionValue& Value);
/** 左键点击移动回调(InputTag.ClickMove 驱动) */
void HandleClickMove();
public:
// ===== T03 新增:Server RPC =====
/** Server RPC - 请求移动到目标位置(服务端验证+权威移动) */
UFUNCTION(Server, Reliable, WithValidation)
void ServerMoveToTarget(FVector TargetLocation);
private:
/** 输入配置资产引用 */
UPROPERTY(EditAnywhere, Category = "Input")
UXiYouInputConfig* InputConfig;
};
SetupInputComponent 更新
cpp
// XiYouPlayerController.cpp - SetupInputComponent 修改后
void AXiYouPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
// 获取自定义 InputComponent
UXiYouInputComponent* XiYouInput = Cast<UXiYouInputComponent>(InputComponent);
if (!XiYouInput || !InputConfig)
{
return;
}
// 绑定 Native 输入动作(Tag 驱动,使用 C++ 原生定义的命名空间变量)
XiYouInput->BindNativeAction(InputConfig, XiYouGameplayTags::InputTag_Move,
ETriggerEvent::Triggered, this, &AXiYouPlayerController::HandleMove);
XiYouInput->BindNativeAction(InputConfig, XiYouGameplayTags::InputTag_ClickMove,
ETriggerEvent::Triggered, this, &AXiYouPlayerController::HandleClickMove);
// 保留 T02 鼠标滚轮轴绑定
InputComponent->BindAxis("MouseWheel", this, &AXiYouPlayerController::OnMouseWheel);
}
设计决策 #5 落地 :移除旧式
BindAction("ClickMove", IE_Pressed, ...),改用 EnhancedInput + InputConfig + GameplayTag 驱动。保留MouseWheel轴绑定(T02 已有,轴映射不通过 EnhancedInput 管理)。
6.3 InputMappingContext 添加
在 BeginPlay() 或 OnPossess() 中添加 IMC_PC 到本地玩家子系统:
cpp
// XiYouPlayerController.cpp - BeginPlay 修改后
void AXiYouPlayerController::BeginPlay()
{
Super::BeginPlay();
// 生成摄像机(T02 已有)
SpawnCamera();
// 添加 PC 输入映射上下文
if (IsLocalController())
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
{
// IMC_PC 资产路径,可通过编辑器配置或硬编码加载
// T10 将根据平台切换 IMC_PC / IMC_Mobile
if (UInputMappingContext* IMC = LoadObject<UInputMappingContext>(
nullptr, TEXT("/Game/Input/IMC_PC.IMC_PC")))
{
Subsystem->AddMappingContext(IMC, 0);
}
}
}
}
6.4 与 CameraActor 的集成
T02 已在 OnPossess() 和 BeginPlay() 中调用 SpawnCamera(),并设置 CameraActor->SetTargetActor(GetPawn())。T03 创建的 AXiYouPlayerCharacter 作为 Pawn 被 Possess 后,摄像机自动跟随,无需额外修改。
七、GameMode 集成
7.1 DefaultPawnClass 设置
设计决策 #9 落地 :移除
bUseSeamlessTravel = true(P0 不需要)。
在 AXiYouGameMode 构造函数中,将 DefaultPawnClass 从 nullptr 改为 AXiYouPlayerCharacter:
cpp
// XiYouGameMode.cpp - T03 修改后
#include "Core/XiYouGameMode.h"
#include "Core/XiYouPlayerController.h"
#include "Core/XiYouPlayerState.h"
#include "Core/XiYouGameState.h"
#include "Core/XiYouHUD.h"
#include "Character/XiYouPlayerCharacter.h" // T03 新增
AXiYouGameMode::AXiYouGameMode()
{
// T03: 设置默认玩家角色类
DefaultPawnClass = AXiYouPlayerCharacter::StaticClass();
// 设置核心框架类(T01 已有)
PlayerControllerClass = AXiYouPlayerController::StaticClass();
PlayerStateClass = AXiYouPlayerState::StaticClass();
GameStateClass = AXiYouGameState::StaticClass();
HUDClass = AXiYouHUD::StaticClass();
// 设计决策 #9:移除 bUseSeamlessTravel = true(P0 不需要)
}
7.2 角色生成逻辑
UE5 的 AGameModeBase 在玩家登录时自动使用 DefaultPawnClass 调用 SpawnDefaultPawnFor() 生成角色实例。T03 阶段使用默认生成逻辑,不自定义 Spawn 位置(后续可在地图中放置 PlayerStart Actor)。
八、文件清单
8.1 新建 C++ 文件
8.2 修改 C++ 文件
| # | 文件路径 | 修改内容 |
|---|---|---|
| 13 | Source/XiYou/Public/Core/XiYouPlayerController.h |
新增 HandleMove、HandleClickMove、ServerMoveToTarget、InputConfig 引用,include XiYouGameplayTags.h |
| 14 | Source/XiYou/Private/Core/XiYouPlayerController.cpp |
实现 Tag 驱动输入绑定(使用 XiYouGameplayTags 命名空间变量)、点击移动、客户端预测+Server RPC、IMC 添加 |
| 15 | Source/XiYou/Public/Core/XiYouPlayerState.h |
添加 IAbilitySystemInterface、ASC/AttributeSet 指针声明 |
| 16 | Source/XiYou/Private/Core/XiYouPlayerState.cpp |
实现 ASC 获取(T03 返回 nullptr) |
| 17 | Source/XiYou/Private/Core/XiYouGameMode.cpp |
设置 DefaultPawnClass,移除 bUseSeamlessTravel |
| 18 | Source/XiYou/XiYou.Build.cs |
检查 GameplayTags 模块(已有,无需修改) |
8.3 修改配置文件
| # | 文件路径 | 修改内容 |
|---|---|---|
| 19 | Config/DefaultInput.ini |
移除旧式 ClickMove ActionMapping(改用 EnhancedInput IA_ClickMove),保留 MouseWheel 轴映射 |
| 20 | Config/DefaultEngine.ini |
添加 CMC 网络平滑配置 + NetworkManager 配置 |
| 21 | Config/DefaultGameplayTags.ini |
补充性质配置(InputTag 已通过 C++ 原生定义,INI 仅保留 GameplayCue 等辅助 Tag 注释) |
DefaultInput.ini 修改
移除旧式 ClickMove ActionMapping(当前文件中无此映射,确认无需删除)。保留 MouseWheel 轴映射(T02 已有):
ini
; 保留鼠标滚轮轴映射(T02 已有)
+AxisMappings=(AxisName="MouseWheel",Key=MouseWheelAxis,Scale=1.000000)
注意 :点击移动改用 EnhancedInput 的
IA_ClickMove,不再需要旧式 ActionMapping。
DefaultEngine.ini 新增配置段
ini
[/Script/Engine.CharacterMovementComponent]
NetworkSimulatedSmoothLocationTime=0.1
NetworkSimulatedSmoothRotationTime=0.1
[/Script/Engine.NetworkManager]
NetServerMaxTickRate=30
MaxClientUpdateRate=30
MaxNetTickRate=30
DefaultGameplayTags.ini 新增
ini
[/Script/GameplayTags.GameplayTagsSettings]
; InputTag 已通过 C++ 原生定义(XiYouGameplayTags.h/.cpp),无需在此重复注册
; 此文件用于补充 GameplayCue 等不需要 C++ 直接引用的辅助 Tag
8.4 蓝图资产(在 UE 编辑器中创建)
| # | 资产路径 | 类型 | 说明 |
|---|---|---|---|
| 20 | Content/Input/IA_Move |
UInputAction |
WASD 移动输入(Value Type: Axis2D) |
| 21 | Content/Input/IA_ClickMove |
UInputAction |
左键点击移动(Value Type: Digital/bool) |
| 22 | Content/Input/IMC_PC |
UInputMappingContext |
PC 端键鼠映射 |
| 23 | Content/Input/DA_XiYouInputConfig |
UXiYouInputConfig |
输入配置 DataAsset |
| 24 | Content/Animations/ABP_XiYouCharacter |
UAnimInstance |
角色动画蓝图(Idle/Walk/Run) |
| 25 | Content/Blueprints/BP_XiYouPlayerCharacter |
AXiYouPlayerCharacter |
玩家角色蓝图 |
详细创建步骤 :见
plans/P0-T03-Blueprint-Guide.md
九、编译验证标准
9.1 编译通过条件
| # | 验证项 | 通过条件 |
|---|---|---|
| 1 | C++ 编译 | Result: Succeeded,无 Error,无 Warning(或仅有 UE5 引擎级 Warning) |
| 2 | 头文件包含 | 所有 #include 路径正确,无循环依赖 |
| 3 | UCLASS/UPROPERTY/UFUNCTION | 宏使用正确,.generated.h 包含正确 |
| 4 | EnhancedInput API | UEnhancedInputComponent、UInputAction 等类型正确引用 |
| 5 | NavigationSystem API | UNavigationSystemV1 正确引用,Build.cs 已含 NavigationSystem 模块 |
| 6 | IAbilitySystemInterface | Character 和 PlayerState 正确实现接口,GetAbilitySystemComponent() override 正确 |
| 7 | InputConfig/InputComponent | UXiYouInputConfig、UXiYouInputComponent 编译通过,模板方法实例化正确 |
9.2 运行验证条件
| # | 验证项 | 通过条件 |
|---|---|---|
| 1 | 角色生成 | PIE 启动后,场景中生成 AXiYouPlayerCharacter 实例 |
| 2 | 摄像机跟随 | T02 摄像机自动跟随新生成的角色(已有逻辑) |
| 3 | WASD 移动 | 按 W/A/S/D,角色向世界坐标对应方向移动(W=+X, S=-X, A=-Y, D=+Y) |
| 4 | 角色朝向 | 移动时角色自动面向移动方向(bOrientRotationToMovement) |
| 5 | 左键点击移动 | 左键点击地面,角色寻路移动到目标点 |
| 6 | 不可达处理 | 点击 NavMesh 外区域/不可达位置,角色不移动(无异常) |
| 7 | 移动中改向 | 移动中再次点击,角色改变目标点 |
| 8 | WASD 取消寻路 | 点击移动后按 WASD,取消寻路切换为手动移动 |
| 9 | 动画切换 | 静止时 Idle,慢走时 Walk,快跑时 Run |
| 10 | DS 同步 | 启动 Dedicated Server + 客户端,移动同步正常,无严重抖动 |
9.3 验证步骤
- 编译: 在 UE 编辑器中 Live Coding 或 VS2022 重新编译项目
- PIE 单机测试: 验证 WASD 移动、左键点击移动、动画切换、不可达处理
- DS 多人测试: 启动 Dedicated Server,连接 2 个客户端,验证移动同步
- NavMesh 验证 : 确保测试地图中放置了
NavMeshBoundsVolume并覆盖可行走区域
十、与前后任务的接口
10.1 T01 依赖(已完成)
| T01 产出 | T03 使用方式 |
|---|---|
AXiYouGameMode |
修改构造函数,设置 DefaultPawnClass |
AXiYouPlayerController |
扩展 Tag 驱动输入绑定、点击移动、Server RPC |
AXiYouPlayerState |
添加 ASC/AttributeSet 预留 + IAbilitySystemInterface |
| Build.cs 模块依赖 | NavigationSystem、EnhancedInput、GameplayTags 已在 T01 中添加 |
10.2 T02 依赖(已完成)
| T02 产出 | T03 使用方式 |
|---|---|
AXiYouCameraActor |
摄像机自动跟随 T03 创建的角色(SetTargetActor(GetPawn()) 已在 T02 实现) |
SpawnCamera() |
T03 角色被 Possess 后自动触发,无需修改 |
10.3 T04 预留接口(ASC 接口设计)
设计决策 #2 落地:ASC 挂载在 PlayerState 上,Character 通过转发获取。
T03 在 AXiYouPlayerState 中预留以下接口供 T04 填充:
cpp
// 1. GAS 组件指针(T03 为 nullptr,T04 创建实例)
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities")
UAbilitySystemComponent* AbilitySystemComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities")
UAttributeSet* AttributeSet;
// 2. IAbilitySystemInterface 接口(T03 返回 nullptr,T04 返回有效指针)
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
T03 在 AXiYouCharacter 中提供转发函数:
cpp
// Character 通过 PlayerState 转发获取 ASC
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
T04 实现预期
T04 将在 AXiYouPlayerState 中:
- 创建
UAbilitySystemComponent实例 - 创建
UAttributeSet实例 - 调用
AbilitySystemComponent->InitAbilityActorInfo() GetAbilitySystemComponent()自动返回有效指针(无需覆写,指针已填充)
10.4 T10 预留接口(手机端输入适配)
T03 的输入系统架构为 T10 预留了扩展点:
cpp
// PlayerController 中 IMC 添加逻辑可扩展为平台切换
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ...)
{
// T10 将根据平台切换 IMC_PC / IMC_Mobile
if (IsRunningOnMobilePlatform())
{
Subsystem->AddMappingContext(IMC_Mobile, 0);
}
else
{
Subsystem->AddMappingContext(IMC_PC, 0);
}
}
T10 将:
- 创建
IMC_Mobile(虚拟摇杆映射) - 运行时检测平台(
FPlatformMisc::IsRunningOnMobilePlatform()) - 切换
InputMappingContext为 Mobile 或 PC 版本 - 添加
IA_Joystick(虚拟摇杆 InputAction)
10.5 T07 预留接口(点击敌人追踪移动 + 自动攻击)
设计背景:手机端点击地面移动与点击敌人到攻击射程内进攻,效果一样(都是点击移动)。T03 只实现"点击地面 → 移动到目标位置"的基础逻辑,"点击敌人 → 移动到攻击射程 → 自动攻击"的完整逻辑由 T07(技能释放与战斗逻辑)实现。
T03 的点击移动系统为 T07 预留了以下扩展点:
1. ServerMoveToTarget → ServerMoveToActor 扩展
当前 T03 的 ServerMoveToTarget 只接受 FVector 目标位置参数(点击地面移动):
cpp
// T03 当前实现(仅位置参数)
UFUNCTION(Server, Reliable, WithValidation)
void ServerMoveToTarget(FVector TargetLocation);
T07 扩展时可增加 AActor* 目标 Actor 参数,实现"追踪目标移动":
cpp
// T07 扩展预期(增加目标 Actor 参数)
UFUNCTION(Server, Reliable, WithValidation)
void ServerMoveToActor(AActor* TargetActor);
追踪目标移动行为:角色持续向目标 Actor 移动,进入攻击射程后停止移动,由 T07 技能释放系统接管(自动攻击)。
2. 点击目标类型判断
点击敌人时,T07 会判断目标类型(地面 vs 敌人),分别走不同逻辑:
| 点击目标 | T03 行为 | T07 扩展行为 |
|---|---|---|
| 点击地面 | ServerMoveToTarget(Location) 寻路移动到位置 |
--- |
| 点击敌人 | ---(T03 不区分) | ServerMoveToActor(Enemy) 追踪移动 + 进入攻击射程后自动攻击 |
3. HandleClickMove 扩展点
T03 的 HandleClickMove() 当前只处理地面点击(GetHitResultUnderCursor 命中地面后寻路)。T07 扩展时,可在射线检测命中 Actor 后判断目标类型:
cpp
// T07 扩展预期伪代码
void AXiYouPlayerController::HandleClickMove()
{
FHitResult HitResult;
GetHitResultUnderCursor(ECC_Pawn, true, HitResult); // 检测 Pawn(敌人)
if (HitResult.GetActor() && IsEnemy(HitResult.GetActor()))
{
// 点击敌人 → 追踪目标移动 + 自动攻击
ServerMoveToActor(HitResult.GetActor());
}
else
{
// 点击地面 → 移动到目标位置(T03 已有逻辑)
// ... T03 原有地面点击移动逻辑 ...
}
}
10.6 T11 预留接口(头顶血条)
设计决策 #4 落地:T03 不做血条,完全留给 T11,不在 Character 中预留任何组件。
T11 将:
- 在
AXiYouCharacter中添加UWidgetComponent(头顶血条) - 创建血条 Widget 蓝图
- 通过 GAS 属性变化委托驱动血条更新
附录:Build.cs 模块依赖确认
当前 XiYou.Build.cs 已包含 T03 所需的全部模块依赖:
| 模块 | 用途 | T03 使用场景 |
|---|---|---|
EnhancedInput |
EnhancedInput 系统 | InputAction 绑定、InputComponent 子类 |
NavigationSystem |
导航系统 | 点击移动 NavMesh 寻路 |
GameplayAbilities |
GAS 系统 | IAbilitySystemInterface、ASC 前向声明 |
GameplayTags |
GameplayTag 系统 | InputTag.Move、InputTag.ClickMove |
NetCore |
网络核心 | Server RPC ServerMoveToTarget |
AIModule |
AI 模块 | AICharacter 空壳(T09 使用) |
结论: T03 无需修改 Build.cs,所有依赖已在 T01 中配置完成。