005 T03 — 角色基类与移动系统 详细功能设计文档

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。移除了角色类中的输入绑定逻辑(MoveActionInputMappingContextSetupPlayerInputComponentOnMoveTriggeredOnMoveCompleted),输入绑定统一由 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.MoveInputTag_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 的配合

AXiYouPlayerControllerSetupInputComponent() 中,绑定 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.HeroIdentity.Enemy
T05 属性系统 Status(状态同步) Shared.Status.DeadPlayer.Status.Attacking
T07 技能释放 Ability、Event、Cooldown Player.Ability.Attack.LightShared.Event.MeleeHit
T08 Buff/Debuff Status(控制效果) State.StunState.SilenceState.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 中"右键点击移动"的要求有意偏差。

理由

  1. ARPG 游戏惯例(如 Diablo、Lost Ark、Path of Exile)均使用左键点击移动
  2. 右键通常预留用于技能释放或目标选择
  3. 左键移动 + 右键技能是玩家肌肉记忆,降低学习成本
  4. PRD 的右键要求可在后续根据用户反馈调整,当前以游戏惯例为准
设计决策 #7:SimpleMoveToLocation + 边界场景处理

决策说明 :使用 UNavigationSystemV1::SimpleMoveToLocation 进行寻路,并特别处理以下不可达边界场景:

  • 点击 NavMesh 外区域(如墙壁内部、空中)
  • 点击不可达位置(被障碍物完全包围)
  • 点击距离过远(防作弊/防异常)
设计决策 #8:客户端本地预测 + 服务端校正

决策说明 :客户端点击后 立即本地 SimpleMoveToLocation (低延迟响应),同时发送 Server RPC 验证。服务端验证通过后执行权威移动,通过 ReplicatedMovement 同步回客户端。

流程

  1. 客户端点击 → 本地 NavMesh 检查 → 本地 SimpleMoveToLocation(即时响应)
  2. 同时发送 ServerMoveToTarget RPC(Reliable)
  3. 服务端验证 → 服务端 SimpleMoveToLocation(权威移动)
  4. ReplicatedMovement 同步 → 客户端平滑插值
交互规则
操作 行为
左键点击地面 角色寻路移动到点击位置
移动中再次左键点击 改变目标点,角色转向新目标
WASD 按下 取消点击移动,切换为手动移动
右键点击 不参与移动(预留技能/目标选择)
手机端适配说明

核心原则 :手机端点击地面移动与 PC 端左键点击地面移动行为完全一致 ,共用同一个 IA_ClickMove InputAction。

手机端点击移动行为:

操作 行为 实现阶段
手机端触摸点击地面 角色寻路移动到触摸位置(与 PC 端左键点击地面行为一致) ✅ T03 实现
手机端触摸点击敌人 角色移动到敌人附近(进入攻击射程后停止移动,由 T07 技能释放系统接管) ⏳ T07 扩展

实现机制:

  1. InputAction 共用 :手机端触摸事件通过 EnhancedInput 的 IA_ClickMove 触发,与 PC 端左键点击共用同一个 InputAction。T03 阶段 IA_ClickMoveIMC_PC 中映射到鼠标左键。

  2. T10 平台映射 :T10(手机端输入适配)会在 IMC_Mobile 中将触摸手势映射到 IA_ClickMove。运行时根据平台自动切换 IMC_PC / IMC_Mobile(详见 §10.4 T10 预留接口)。

  3. T03 阶段范围:T03 只实现"点击地面 → 移动到目标位置"的基础逻辑。"点击敌人 → 移动到攻击射程 → 自动攻击"的完整逻辑由 T07(技能释放与战斗逻辑)实现。

  4. 目标 Actor 参数预留 :T03 的点击移动系统需要预留目标 Actor 参数(而非仅 FVector 目标位置),以便 T07 扩展为"追踪目标移动"。详见下方 ServerMoveToTarget RPC 扩展说明和 §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 创建步骤

  1. 获取 Lyra 或 Fab 免费动画资产(Idle/Walk/Run)
  2. 在 UE 编辑器中创建 AnimBP:基于 UE5 Mannequin 骨骼
  3. 创建状态机:IdleWalkRun 三个状态
  4. 每个状态使用 Play Anim Sequence 节点播放对应动画
  5. 转换条件基于 Speed 变量(Event Blueprint Update Animation 中更新)
  6. 在角色蓝图 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 已有)

AXiYouPlayerController 已有:

  • bShowMouseCursor = true / bEnableClickEvents = true / bEnableTouchEvents = true
  • CameraActor 引用(UPROPERTY(VisibleAnywhere)
  • SpawnCamera() --- 生成摄像机并设置 ViewTarget
  • OnPossess() / 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 构造函数中,将 DefaultPawnClassnullptr 改为 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++ 文件

# 文件路径 类型 职责说明
1 Source/XiYou/Public/Character/XiYouCharacter.h C++ 头文件 角色基类声明,含 IAbilitySystemInterface、移动配置
2 Source/XiYou/Private/Character/XiYouCharacter.cpp C++ 源文件 角色基类实现,构造函数配置移动组件,ASC 转发
3 Source/XiYou/Public/Character/XiYouPlayerCharacter.h C++ 头文件 玩家角色声明,含 MoveToLocation/StopClickMove
4 Source/XiYou/Private/Character/XiYouPlayerCharacter.cpp C++ 源文件 玩家角色实现,NavMesh 寻路
5 Source/XiYou/Public/Character/XiYouAICharacter.h C++ 头文件 AI角色基类空壳占位
6 Source/XiYou/Private/Character/XiYouAICharacter.cpp C++ 源文件 AI角色基类空壳实现
7 Source/XiYou/Public/Core/XiYouInputConfig.h C++ 头文件 输入配置 DataAsset,InputAction→Tag 映射
8 Source/XiYou/Private/Core/XiYouInputConfig.cpp C++ 源文件 输入配置实现,Tag 查找函数
9 Source/XiYou/Public/Core/XiYouInputComponent.h C++ 头文件 自定义输入组件,BindNativeAction 模板方法
10 Source/XiYou/Private/Core/XiYouInputComponent.cpp C++ 源文件 输入组件实现
11 Source/XiYou/Public/Core/XiYouGameplayTags.h C++ 头文件 GameplayTag C++ 原生定义(InputTag.Move、InputTag.ClickMove)
12 Source/XiYou/Private/Core/XiYouGameplayTags.cpp C++ 源文件 GameplayTag C++ 原生定义实现(UE_DEFINE_GAMEPLAY_TAG_COMMENT)

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 UEnhancedInputComponentUInputAction 等类型正确引用
5 NavigationSystem API UNavigationSystemV1 正确引用,Build.cs 已含 NavigationSystem 模块
6 IAbilitySystemInterface Character 和 PlayerState 正确实现接口,GetAbilitySystemComponent() override 正确
7 InputConfig/InputComponent UXiYouInputConfigUXiYouInputComponent 编译通过,模板方法实例化正确

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 验证步骤

  1. 编译: 在 UE 编辑器中 Live Coding 或 VS2022 重新编译项目
  2. PIE 单机测试: 验证 WASD 移动、左键点击移动、动画切换、不可达处理
  3. DS 多人测试: 启动 Dedicated Server,连接 2 个客户端,验证移动同步
  4. NavMesh 验证 : 确保测试地图中放置了 NavMeshBoundsVolume 并覆盖可行走区域

十、与前后任务的接口

10.1 T01 依赖(已完成)

T01 产出 T03 使用方式
AXiYouGameMode 修改构造函数,设置 DefaultPawnClass
AXiYouPlayerController 扩展 Tag 驱动输入绑定、点击移动、Server RPC
AXiYouPlayerState 添加 ASC/AttributeSet 预留 + IAbilitySystemInterface
Build.cs 模块依赖 NavigationSystemEnhancedInputGameplayTags 已在 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 中:

  1. 创建 UAbilitySystemComponent 实例
  2. 创建 UAttributeSet 实例
  3. 调用 AbilitySystemComponent->InitAbilityActorInfo()
  4. 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 将:

  1. 创建 IMC_Mobile(虚拟摇杆映射)
  2. 运行时检测平台(FPlatformMisc::IsRunningOnMobilePlatform()
  3. 切换 InputMappingContext 为 Mobile 或 PC 版本
  4. 添加 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 将:

  1. AXiYouCharacter 中添加 UWidgetComponent(头顶血条)
  2. 创建血条 Widget 蓝图
  3. 通过 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 中配置完成。

相关推荐
AA陈超21 小时前
004 T02 - 俯视角摄像机系统 设计文档
网络·c++·ue5·虚幻引擎
AA陈超1 天前
006 T03 — 蓝图操作指南
c++·游戏·架构·ue5·github·虚幻引擎
Duo1J3 天前
【UE】Slate 编辑器工具开发01 - Slate语法 & 常用控件
ue5·编辑器·游戏引擎·ue4
朗迹 - 张伟4 天前
UE5.7 PCG从入门到精通系列课程【第五课:PCG沿一条样条线生成物体】
ue5·pcg
AA陈超4 天前
003 XiYou 西游 — P0 阶段实施计划
c++·笔记·学习·ue5
朗迹 - 张伟5 天前
UE5.7 PCG从入门到精通系列课程【第三课:程序化生成森林】
ue5·pcg
朗迹 - 张伟6 天前
UE5.7 PCG从入门到精通系列课程【第二课:使用PCG制作草地系统】
ue5·pcg
im长街6 天前
UE 课程介绍 - 1. 3DMax软件建模基础
ue5·3dsmax
directx3d_beginner8 天前
25,怪物受伤害和受力
ue5