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

三种方法:

方法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 输入系统的工程化设计原则,能让你的代码从 "能跑" 变成 "易维护、可扩展"。

相关推荐
速冻鱼Kiel3 小时前
GASP笔记01
笔记·ue5·游戏引擎·虚幻
孟无岐4 小时前
【Laya】Animator2D 使用指南
typescript·游戏引擎·游戏程序·laya
速冻鱼Kiel6 小时前
GASP笔记02
笔记·ue5·游戏引擎·虚幻
__water6 小时前
RHK《Unity接入PicoSDK入门》
unity·游戏引擎·picosdk
我的golang之路果然有问题7 小时前
unity 资源导入 godot
unity·游戏引擎·godot
迪普阳光开朗很健康7 小时前
Unity+Vscode+EmmyLua+XLua 调试实战
vscode·unity·游戏引擎
Howrun7777 小时前
虚幻引擎_UI搭建流程
c++·游戏引擎·虚幻
CreasyChan8 小时前
Unity 中的 IEnumerator协程详解
unity·c#·游戏引擎
熬夜敲代码的小N8 小时前
基于Unity开发Pico VR眼镜基础应用:从环境搭建到实战部署全解析
人工智能·unity·游戏引擎·vr