虚幻引擎_控制角色移动的三种方法

三种方法:

方法1:

  1. 在控制器类中激活输入映射上下文

  2. 绑定输入动作和响应函数(这里是移动相关的)

  3. 在游戏模式类中绑定控制器和Tank角色类

方法2:

在玩家控制器类中:

  1. 激活输入映射上下文

  2. 绑定输入动作和响应函数(这里是移动相关的)

  3. 将此玩家控制器绑定到Tank角色类

缺点: 不利于控制器的复用, 这个控制器类写死了, 只能控制角色Tank;

方法3:

在Tank角色中:

  1. 激活输入映射上下文

  2. 绑定输入动作和响应函数(这里是移动相关的)

方法4:

与方法1类似;

三种方法核心差异总览

对比维度 方法 1(控制器管输入 + GameMode 绑角色) 方法 2(控制器包揽输入 + 绑角色) 方法 3(Tank 管所有输入)
职责划分 ✅ 控制器:输入解析 / GameMode:角色分配 / Tank:动作执行(单一职责) ❌ 控制器包揽输入 + 角色绑定(职责过重) ❌ Tank 包揽输入 + 动作(职责混乱)
控制器复用性 ✅ 极高(控制器不依赖具体角色,可控制坦克 / 步兵 / 飞机) ❌ 极低(写死 Tank,换角色需重写控制器) ❌ 无(输入逻辑和 Tank 强绑定)
角色绑定灵活性 ✅ 极高(GameMode 可按规则动态绑定,支持单人 / 本地多人 / 线上) ❌ 极低(控制器写死生成 / 绑定 Tank) ⚠️ 低(依赖外部绑定控制器,输入逻辑在 Tank)
稳定性 ✅ 高(控制器激活 IMC 时机稳定,GameMode 可延迟绑定避免空指针) ⚠️ 中(绑定时机稳定,但扩展差) ❌ 低(Tank 激活 IMC 时控制器可能未绑定)
多人 / 线上适配性 ✅ 完美适配(GameMode 服务器权威绑定,控制器仅本地处理输入) ❌ 适配差(控制器写死 Tank,无法按索引绑定多玩家) ❌ 适配差(易重复激活 IMC,无法区分本地 / 远程)
维护成本 ✅ 低(改角色仅改 GameMode,输入逻辑复用) ❌ 高(改角色需改控制器核心逻辑) ❌ 极高(每个角色都要写输入逻辑)

完整代码实现(基于 UE 5.x C++)

通用前置准备

  1. 在 UE 编辑器创建:
    • 输入映射上下文(IMC):TankMoveIMC(包含移动动作MoveAction,绑定 WASD / 方向键);
    • Tank 类:AMyTank(继承 APawn,实现Move移动方法)。

方法 1:控制器管输入 + GameMode 绑角色(最优解)

核心逻辑

  • 控制器:只做 "通用输入解析"(激活 IMC、绑定输入动作),不依赖任何具体角色;
  • GameMode:只做 "角色分配"(按规则绑定控制器和 Tank),不处理输入;
  • Tank:只做 "动作执行"(实现移动逻辑),不碰输入。

1. 玩家控制器代码(AMyPlayerController)

头文件(AMyPlayerController.h)

cpp 复制代码
#pragma once
#include "CoreMinimal.h"
#include "PlayerController.h"
#include "MyPlayerController.generated.h"

class UInputMappingContext;
class UInputAction;

UCLASS()
class YOUR_PROJECT_NAME_API AMyPlayerController : public APlayerController
{
    GENERATED_BODY()

protected:
    // 游戏开始时激活IMC、绑定输入动作
    virtual void BeginPlay() override;

private:
    // 通用移动输入映射上下文(不绑定具体角色)
    UPROPERTY(EditAnywhere, Category = "Input")
    UInputMappingContext* MoveIMC;

    // 移动输入动作
    UPROPERTY(EditAnywhere, Category = "Input")
    UInputAction* MoveAction;

    // 通用移动响应函数(适配所有有移动能力的角色)
    void OnMove(const FInputActionValue& InputValue);
};

实现文件(AMyPlayerController.cpp)

cpp 复制代码
#include "MyPlayerController.h"
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
#include "Kismet/GameplayStatics.h"

void AMyPlayerController::BeginPlay()
{
    Super::BeginPlay();

    // 1. 激活输入映射上下文(仅本地控制器执行,避免服务器/远程客户端执行)
    if (IsLocalPlayerController() && MoveIMC)
    {
        ULocalPlayer* LocalPlayer = GetLocalPlayer();
        UEnhancedInputLocalPlayerSubsystem* InputSubsystem = 
            LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
        if (InputSubsystem)
        {
            InputSubsystem->AddMappingContext(MoveIMC, 0); // 优先级0
        }
    }

    // 2. 绑定输入动作到响应函数(通用逻辑,不依赖具体角色)
    if (UEnhancedInputComponent* EnhancedInputComp = 
        Cast<UEnhancedInputComponent>(InputComponent))
    {
        EnhancedInputComp->BindAction(MoveAction, ETriggerEvent::Triggered, 
            this, &AMyPlayerController::OnMove);
    }
}

void AMyPlayerController::OnMove(const FInputActionValue& InputValue)
{
    // 通用获取当前控制的角色,不写死Tank(后续可扩展为接口)
    APawn* ControlledPawn = GetPawn();
    if (!ControlledPawn) return;

    // 解析输入(控制器的核心职责)
    FVector2D MoveDir = InputValue.Get<FVector2D>();

    // 这里使用了 Cast,如果你有多种载具,建议使用接口 (Interface)
    if (AMyTank* MyTank = Cast<AMyTank>(ControlledPawn))
    {
        // 控制器只负责传递意图:"嘿,坦克,往这个方向动!"
        MyTank->Move(MoveDir); 
    }
    // 如果以后有直升机:
    // else if (AMyHeli* MyHeli = Cast<AMyHeli>(ControlledPawn)) { ... }
}
进阶:引入接口(IMoveable),适配多角色(这里我们仅仅提及)

如果后续想加飞机、步兵等可移动角色,还可以定义一个「可移动接口」,让所有可移动角色实现该接口,控制器无需关心具体是哪个角色:

cpp 复制代码
// 定义IMoveable接口(头文件)
UINTERFACE(MinimalAPI)
class UMoveable : public UInterface
{
    GENERATED_BODY()
};

class IMoveable
{
    GENERATED_BODY()
public:
    virtual void Move(const FVector2D& MoveDir) = 0; // 纯虚函数,强制实现
};

// Tank类继承该接口
UCLASS()
class YOUR_PROJECT_NAME_API AMyTank : public APawn, public IMoveable
{
    GENERATED_BODY()
public:
    virtual void Move(const FVector2D& MoveDir) override; // 实现接口
};

// 控制器里通过接口调用,适配所有可移动角色
void AMyPlayerController::OnMove(const FInputActionValue& InputValue)
{
    APawn* ControlledPawn = GetPawn();
    if (!ControlledPawn) return;

    FVector2D MoveDir = InputValue.Get<FVector2D>();
    // 不管是坦克、飞机还是步兵,只要实现IMoveable就可以调用Move()
    IMoveable* MoveableActor = Cast<IMoveable>(ControlledPawn);
    if (MoveableActor)
    {
        MoveableActor->Move(MoveDir);
    }
}

2. 游戏模式代码(AMyGameMode)

头文件(AMyGameMode.h)

cpp 复制代码
#pragma once
#include "CoreMinimal.h"
#include "GameModeBase.h"
#include "MyGameMode.generated.h"

UCLASS()
class YOUR_PROJECT_NAME_API AMyGameMode : public AGameModeBase
{
    GENERATED_BODY()

protected:
    virtual void BeginPlay() override;

private:
    // Tank类模板(可在编辑器配置)
    UPROPERTY(EditAnywhere, Category = "Character")
    TSubclassOf<APawn> TankClass;

    // 绑定控制器和Tank的通用函数
    void BindControllerToTank(int32 PlayerIndex, const FVector& SpawnPos);
};

实现文件(AMyGameMode.cpp)

cpp 复制代码
#include "MyGameMode.h"
#include "MyPlayerController.h"
#include "MyTank.h"

void AMyGameMode::BeginPlay()
{
    Super::BeginPlay();
    // 单人模式:绑定玩家0到Tank(位置0,0,0)
    BindControllerToTank(0, FVector(0, 0, 0));

    // 本地多人模式示例:绑定玩家1到Tank(位置100,0,0)
    // FTimerHandle DelayHandle;
    // GetWorld()->GetTimerManager().SetTimer(DelayHandle, [this]() {
    //     BindControllerToTank(1, FVector(100, 0, 0));
    // }, 0.1f, false);
}
//此函数通过角色索引, 以及生成角色的位置, 将角色索引搭载的控制器绑定到该位置生成的角色上
void AMyGameMode::BindControllerToTank(int32 PlayerIndex, const FVector& SpawnPos)
{
    // 获取对应索引的玩家控制器
    APlayerController* TargetPC = GetWorld()->GetPlayerController(PlayerIndex);
    if (!TargetPC || !TankClass) return;

    // 在某位置生成Tank(服务器权威,线上多人也适用)
    APawn* SpawnedTank = GetWorld()->SpawnActor<APawn>(TankClass, SpawnPos, FRotator::ZeroRotator);
    if (SpawnedTank)
    {
        // 绑定控制器和Tank(服务器执行,同步到所有客户端)
        TargetPC->Possess(SpawnedTank);
    }
}

3. Tank 类代码(仅实现移动,无输入逻辑)

头文件(AMyTank.h)

cpp 复制代码
#pragma once
#include "CoreMinimal.h"
#include "Pawn.h"
#include "MyTank.generated.h"

UCLASS()
class YOUR_PROJECT_NAME_API AMyTank : public APawn
{
    GENERATED_BODY()

public:
    virtual void Move(const FVector2D& MoveDir);
};

实现文件(AMyTank.cpp)

cpp 复制代码
#include "MyTank.h"

void AMyTank::Move(const FVector2D& MoveDir)
{
    // 移动逻辑
    FVector WorldMoveDir = (GetActorRightVector() * MoveDir.X) + (GetActorForwardVector() * MoveDir.Y);
    AddActorWorldOffset(WorldMoveDir * 100 * GetWorld()->GetDeltaSeconds(), true);
}

方法 1 核心优点(重点回答你的问题)

  1. 控制器 100% 复用 :控制器只处理 "输入解析",不关心控制的是坦克、步兵还是飞机 ------ 只要新角色实现移动逻辑(比如封装IMoveable接口),控制器无需修改就能控制;
  2. 角色绑定灵活:GameMode 可按任意规则绑定(单人 / 本地多人 / 线上、随机出生点 / 指定标签),控制器完全无感知;
  3. 符合服务器权威 :线上多人时,GameMode 在服务器执行Possess,控制器仅本地处理输入,避免网络同步问题;
  4. 维护成本极低:改按键仅改控制器的 IMC / 输入动作,改角色仅改 GameMode 的绑定逻辑,互不影响。

方法 2:控制器包揽输入 + 绑角色(复用性差)

核心逻辑

控制器不仅激活 IMC、绑定输入动作,还直接生成 / 绑定 Tank------ 控制器和 Tank 强耦合,无法复用。

控制器代码(AMyPlayerController.cpp,仅修改 BeginPlay)

cpp 复制代码
void AMyPlayerController::BeginPlay()
{
    Super::BeginPlay();

    // 1. 激活IMC(和方法1一致)
    if (IsLocalPlayerController() && MoveIMC)
    {
        ULocalPlayer* LocalPlayer = GetLocalPlayer();
        UEnhancedInputLocalPlayerSubsystem* InputSubsystem = 
            LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
        if (InputSubsystem) InputSubsystem->AddMappingContext(MoveIMC, 0);
    }

    // 2. 绑定输入动作(和方法1一致)
    if (UEnhancedInputComponent* EnhancedInputComp = Cast<UEnhancedInputComponent>(InputComponent))
    {
        EnhancedInputComp->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyPlayerController::OnMove);
    }

    // 3. 控制器直接生成并绑定Tank(写死,复用性差)
    UPROPERTY(EditAnywhere, Category = "Character")
    TSubclassOf<AMyTank> TankClass; // 需在头文件添加此变量
    
    if (TankClass)
    {
        AMyTank* SpawnedTank = GetWorld()->SpawnActor<AMyTank>(TankClass, FVector(0,0,0), FRotator::ZeroRotator);
        if (SpawnedTank) Possess(SpawnedTank); // 写死绑定Tank
    }
}

方法 2 核心缺点

  • 控制器和 Tank 强绑定:想控制步兵,必须修改控制器的SpawnActorCast逻辑,完全无法复用;
  • 角色绑定无统一管理:多人模式下,每个控制器都自己生成 Tank,无法集中管理出生点、数量等规则;
  • 线上多人适配差:客户端控制器生成 Tank 会导致服务器 / 客户端状态不一致。

方法 3:Tank 内部处理所有输入(职责混乱)

核心逻辑

Tank 自己激活 IMC、绑定输入动作 ------ 输入逻辑和角色强耦合,稳定性差。

Tank 类代码(AMyTank.cpp)

cpp 复制代码
#include "MyTank.h"
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
#include "PlayerController.h"

UCLASS()
class YOUR_PROJECT_NAME_API AMyTank : public APawn
{
    GENERATED_BODY()

protected:
    virtual void BeginPlay() override;

private:
    UPROPERTY(EditAnywhere, Category = "Input")
    UInputMappingContext* TankIMC;

    UPROPERTY(EditAnywhere, Category = "Input")
    UInputAction* MoveAction;

    void OnMove(const FInputActionValue& InputValue);
};

void AMyTank::BeginPlay()
{
    Super::BeginPlay();

    // 1. Tank自己激活IMC(不稳定:控制器可能未绑定)
    APlayerController* PC = Cast<APlayerController>(GetController());
    if (PC && PC->IsLocalPlayerController() && TankIMC)
    {
        ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
        UEnhancedInputLocalPlayerSubsystem* InputSubsystem = 
            LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
        if (InputSubsystem) InputSubsystem->AddMappingContext(TankIMC, 0);
    }

    // 2. Tank自己绑定输入动作(InputComponent可能未初始化)
    if (UEnhancedInputComponent* EnhancedInputComp = Cast<UEnhancedInputComponent>(InputComponent))
    {
        EnhancedInputComp->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyTank::OnMove);
    }
}

void AMyTank::OnMove(const FInputActionValue& InputValue)
{
    // 直接在Tank中处理移动
    FVector2D MoveDir = InputValue.Get<FVector2D>();
    FVector WorldMoveDir = (GetActorRightVector() * MoveDir.X) + (GetActorForwardVector() * MoveDir.Y);
    AddActorWorldOffset(WorldMoveDir * 100 * GetWorld()->GetDeltaSeconds(), true);
}

方法 3 核心缺点

  • 稳定性差:Tank 的BeginPlay执行时,控制器可能还没Possess,导致GetController()返回空,IMC 激活失败;
  • 职责混乱:Tank 既负责 "移动" 又负责 "输入解析",违背 UE"控制器处理输入、角色执行动作" 的设计原则;
  • 复用性为 0:每个角色(坦克 / 步兵)都要重复写激活 IMC、绑定输入的逻辑,改按键需改所有角色。

方法四:

在游戏模式中为玩家激活IMC, 在玩家控制器类为IMC的动作绑定响应函数

创建一个通用的玩家控制器类(AMySplitScreenPlayerController),专门处理输入动作绑定和移动响应:

1. 控制器头文件(AMyPlayerController.h)

cpp 复制代码
#pragma once
#include "CoreMinimal.h"
#include "PlayerController.h"
#include "MyPlayerController.generated.h"

// 提前声明输入动作和坦克类
class UInputAction;
class AMyTank;

UCLASS()
class YOUR_PROJECT_NAME_API AMyPlayerController : public APlayerController
{
    GENERATED_BODY()

protected:
    // 游戏开始时绑定输入动作
    virtual void BeginPlay() override;

private:
    // 移动输入动作(在UE编辑器里绑定WASD/方向键)
    UPROPERTY(EditAnywhere, Category = "Input")
    UInputAction* MoveAction;

    // 移动响应函数(核心:把输入指令传给坦克)
    void HandleTankMove(const FInputActionValue& InputValue);
};

2. 控制器实现文件(AMyPlayerController.cpp)

cpp 复制代码
#include "MyPlayerController.h"
#include "MyTank.h"
#include "EnhancedInputComponent.h"

void AMyPlayerController::BeginPlay()
{
    Super::BeginPlay();

    // 核心:绑定输入动作到响应函数(只在本地控制器执行)
    if (IsLocalPlayerController() && MoveAction)
    {
        if (UEnhancedInputComponent* EnhancedInputComp = Cast<UEnhancedInputComponent>(InputComponent))
        {
            // 绑定"移动动作"到"HandleTankMove"函数
            EnhancedInputComp->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyPlayerController::HandleTankMove);
        }
    }
}

void AMyPlayerController::HandleTankMove(const FInputActionValue& InputValue)
{
    // 1. 获取当前控制器绑定的坦克
    AMyTank* ControlledTank = Cast<AMyTank>(GetPawn());
    if (!ControlledTank) return;

    // 2. 提取输入方向(2D向量:X=左右,Y=前后)
    FVector2D MoveDir = InputValue.Get<FVector2D>();

    // 3. 把移动指令传给坦克(调用坦克的移动函数)
    ControlledTank->Move(MoveDir);
}

坦克类只负责 "执行移动",不用碰输入逻辑:

Tank 头文件(AMyTank.h)

cpp 复制代码
#pragma once
#include "CoreMinimal.h"
#include "Pawn.h"
#include "MyTank.generated.h"

UCLASS()
class YOUR_PROJECT_NAME_API AMyTank : public APawn
{
    GENERATED_BODY()

public:
    // 坦克的移动函数(被控制器调用)
    UFUNCTION()
    void Move(const FVector2D& MoveDir);
};

Tank 实现文件(AMyTank.cpp)

cpp 复制代码
#include "MyTank.h"

void AMyTank::Move(const FVector2D& MoveDir)
{
    // 实际移动逻辑:按输入方向偏移坦克位置
    FVector DeltaLoc = FVector(MoveDir.X, MoveDir.Y, 0) * 100 * GetWorld()->GetDeltaSeconds();
    AddActorLocalOffset(DeltaLoc, true); // 移动坦克
}

第三步: GameMode 代码(加延迟 + 确保控制器类型正确)

原 GameMode 代码有个坑:GetPlayerController(1) 在 BeginPlay 阶段可能还没创建,会导致空指针。补全后:

cpp 复制代码
void AMyGameMode::BeginPlay()
{
    Super::BeginPlay();
    // 1. 启用分屏
    APlayerController* PC0 = GetWorld()->GetFirstPlayerController();
    PC0->SetSplitScreenMode(ESplitScreenMode::TwoPlayers_Horizontal);

    // 2. 玩家0:生成坦克+绑定控制器+激活IMC(WASD)
    AMyTank* Tank0 = SpawnActor<AMyTank>(TankClass, FVector(0,0,0), FRotator::ZeroRotator);
    PC0->Possess(Tank0);
    ActivateIMCForPlayer(0, IMC_Player0);

    // 3. 延迟0.1秒处理玩家1(避免PC1为空)
    FTimerHandle DelayHandle;
    GetWorld()->GetTimerManager().SetTimer(DelayHandle, [this]()
    {
        APlayerController* PC1 = GetWorld()->GetPlayerController(1);
        if (PC1)
        {
            // 玩家1:生成坦克+绑定控制器+激活IMC(方向键)
            AMyTank* Tank1 = SpawnActor<AMyTank>(TankClass, FVector(100,0,0), FRotator::ZeroRotator);
            PC1->Possess(Tank1);
            ActivateIMCForPlayer(1, IMC_Player1);
        }
    }, 0.1f, false);
}

// 原激活IMC函数不变
void AMyGameMode::ActivateIMCForPlayer(int32 PlayerIndex, UInputMappingContext* IMC)
{
    APlayerController* PC = GetWorld()->GetPlayerController(PlayerIndex);
    if (!PC) return;
    ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
    UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(LocalPlayer);
    Subsystem->AddMappingContext(IMC, 0);
}

关键分工说明(为什么要这么拆)

负责的活儿 不碰的活儿
GameMode 开分屏、生成坦克、绑定控制器、激活 IMC 输入动作绑定、移动逻辑
PlayerController 绑定输入动作、解析输入、调用坦克移动 生成坦克、分配控制器
Tank 执行移动(收到控制器指令就动) 输入解析、IMC 激活、控制器绑定

三、总结

方法 核心价值 适用场景 最终建议
方法 1 解耦、复用、适配全场景 所有正式项目(单人 / 本地多人 / 线上) ✅ 首选
方法 2 逻辑集中,Demo 开发快 单人极简 Demo(仅控制 Tank,不扩展) ⚠️ 临时使用
方法 3 新手易上手,无需跨类 临时测试(仅一个角色,不迭代) ❌ 禁止用于正式项目

核心口诀:控制器管 "输入解析",GameMode 管 "角色分配",角色管 "动作执行"------ 这是 UE 输入系统的工程化设计原则,能让你的代码从 "能跑" 变成 "易维护、可扩展"。

相关推荐
郑寿昌10 小时前
UE5与UE6在Lumen和Nanite的差异解析
游戏引擎·图形渲染·着色器
郑寿昌20 小时前
UE6 AI加速Lumen光线追踪降噪技术解析
人工智能·游戏引擎
晴夏。20 小时前
GAS下的网络同步的全面分析【超级全面】
游戏引擎·ue·gas·网络同步
田鸡_20 小时前
Unity新输入系统(Input System)教学篇
unity·游戏引擎·游戏程序
EQ-雪梨蛋花汤20 小时前
【Unity笔记】Unity 音游模板与免费资源:高效构建节奏游戏开发全指南
笔记·unity·游戏引擎
微莱羽墨21 小时前
零、0基础入门Unity 安装详细教程(2026最新版教程,安装Unity看这一篇就够了!)
unity·游戏引擎·unity安装
nnsix1 天前
Unity 刚体的 默认力、瞬时力 区别
unity·游戏引擎
nnsix1 天前
Unity Sprite的 Generate Physics Shape 参数解释
unity·游戏引擎
魔士于安1 天前
Unity完整小球迷宫项目
前端·unity·游戏引擎·贴图·模型
め.1 天前
Unity协程的原理
unity·游戏引擎