feat: 网络同步角色移动

This commit is contained in:
2025-12-05 17:16:12 +08:00
parent 5187e3c0a2
commit f5abca6ffa
15 changed files with 417 additions and 121 deletions

View File

@@ -1,63 +0,0 @@
// Copyright Majowaveon Games. All Rights Reserved.
#include "SuriverPlayer.h"
#include "HealthComponent.h"
#include "GameFramework/Actor.h"
/**
* @brief ASuriverPlayer的构造函数。
*/
ASuriverPlayer::ASuriverPlayer() {
// 允许该角色每帧调用Tick()
PrimaryActorTick.bCanEverTick = true;
// 创建并附加生命组件
HealthComponent = CreateDefaultSubobject<UHealthComponent>(TEXT("HealthComponent"));
}
/**
* @brief 游戏开始或角色生成时调用。
*/
void ASuriverPlayer::BeginPlay() {
Super::BeginPlay();
if (HealthComponent) {
HealthComponent->OnDeath.AddDynamic(this, &ASuriverPlayer::OnPlayerDied);
}
}
/**
* @brief 每帧调用。
* @param DeltaTime 帧间隔时间。
*/
void ASuriverPlayer::Tick(float DeltaTime) {
Super::Tick(DeltaTime);
}
/**
* @brief 绑定输入功能。
* @param PlayerInputComponent 用于绑定输入的组件。
*/
void ASuriverPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) {
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
/**
* @brief 接收伤害的实现。
* @param DamageAmount 伤害量。
* @param InstigatorActor 造成伤害的Actor。
*/
void ASuriverPlayer::ReceiveDamage(float DamageAmount, AActor* InstigatorActor) {
if (HealthComponent) {
// 通过生命组件处理伤害
HealthComponent->HandleDamage(DamageAmount);
}
}
/**
* @brief 当玩家死亡时调用,打印日志。
* @todo 通知 GameMode 玩家已死亡,处理结算逻辑
*/
void ASuriverPlayer::OnPlayerDied() {
UE_LOG(LogTemp, Warning, TEXT("你死了"));
}

View File

@@ -1,54 +0,0 @@
// Copyright Majowaveon Games. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "DamageableInterface.h"
#include "GameFramework/Character.h"
#include "SuriverPlayer.generated.h"
class UHealthComponent;
/**
* @class ASuriverPlayer
* @brief 玩家角色基类,实现了可受伤害接口。
* @ingroup Battle
*/
UCLASS()
class FIRSTPERSONDEMO_API ASuriverPlayer : public ACharacter, public IDamageableInterface {
GENERATED_BODY()
public:
/**
* @brief 构造函数,设置角色的默认属性。
*/
ASuriverPlayer();
protected:
/**
* @brief 管理角色生命值的组件。
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UHealthComponent* HealthComponent;
virtual void BeginPlay() override;
/**
* @brief 当玩家死亡时调用。
* @todo 添加死亡处理逻辑如UI弹窗等
*/
UFUNCTION()
void OnPlayerDied();
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
/**
* @brief 接收伤害的接口实现。
* @param DamageAmount 伤害量。
* @param InstigatorActor 造成伤害的Actor。
*/
virtual void ReceiveDamage(float DamageAmount, AActor* InstigatorActor) override;
};

View File

@@ -0,0 +1,169 @@
// Copyright Majowaveon Games. All Rights Reserved.
#include "SurviverPlayer.h"
#include "HealthComponent.h"
#include "GameFramework/Actor.h"
#include "Animation/AnimInstance.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "EnhancedInputComponent.h"
#include "InputActionValue.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "FirstPersonDemo.h"
/**
* @brief ASuriverPlayer的构造函数。
*/
ASurviverPlayer::ASurviverPlayer() {
// 网络同步设置
bReplicates = true;
GetCharacterMovement()->SetIsReplicated(true);
// 允许该角色每帧调用Tick()
PrimaryActorTick.bCanEverTick = true;
// 创建并附加生命组件
HealthComponent = CreateDefaultSubobject<UHealthComponent>(TEXT("HealthComponent"));
// Set size for collision capsule
GetCapsuleComponent()->InitCapsuleSize(55.f, 96.0f);
// Create the first person mesh that will be viewed only by this character's owner
CharacterMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("角色 Mesh"));
CharacterMesh->SetupAttachment(GetMesh());
// CharacterMesh->SetOnlyOwnerSee(true);
// CharacterMesh->FirstPersonPrimitiveType = EFirstPersonPrimitiveType::FirstPerson;
CharacterMesh->SetCollisionProfileName(FName("NoCollision"));
// Create the Camera Component
CharacterCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("First Person Camera"));
CharacterCameraComponent->SetupAttachment(CharacterMesh, FName("head"));
CharacterCameraComponent->
SetRelativeLocationAndRotation(FVector(-2.8f, 5.89f, 0.0f), FRotator(0.0f, 90.0f, -90.0f));
CharacterCameraComponent->bUsePawnControlRotation = true;
CharacterCameraComponent->bEnableFirstPersonFieldOfView = true;
CharacterCameraComponent->bEnableFirstPersonScale = true;
CharacterCameraComponent->FirstPersonFieldOfView = 70.0f;
CharacterCameraComponent->FirstPersonScale = 0.6f;
// configure the character comps
GetMesh()->SetOwnerNoSee(true);
GetMesh()->FirstPersonPrimitiveType = EFirstPersonPrimitiveType::WorldSpaceRepresentation;
GetCapsuleComponent()->SetCapsuleSize(34.0f, 96.0f);
// Configure character movement
GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;
GetCharacterMovement()->AirControl = 0.5f;
}
/**
* @brief 游戏开始或角色生成时调用。
*/
void ASurviverPlayer::BeginPlay() {
Super::BeginPlay();
if (HealthComponent) {
HealthComponent->OnDeath.AddDynamic(this, &ASurviverPlayer::OnPlayerDied);
}
}
/**
* @brief 每帧调用。
* @param DeltaTime 帧间隔时间。
*/
void ASurviverPlayer::Tick(float DeltaTime) {
Super::Tick(DeltaTime);
}
/**
* @brief 绑定输入功能。
* @param PlayerInputComponent 用于绑定输入的组件。
*/
void ASurviverPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) {
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
// Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ASurviverPlayer::DoJumpStart);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ASurviverPlayer::DoJumpEnd);
// Moving
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ASurviverPlayer::MoveInput);
// Looking/Aiming
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ASurviverPlayer::LookInput);
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this,
&ASurviverPlayer::LookInput);
}
else {
UE_LOG(LogFirstPersonDemo, Error,
TEXT(
"'%s' Failed to find an Enhanced Input Component! This template is built to use the Enhanced Input system. If you intend to use the legacy system, then you will need to update this C++ file."
), *GetNameSafe(this));
}
}
void ASurviverPlayer::MoveInput(const FInputActionValue& Value) {
// get the Vector2D move axis
FVector2D MovementVector = Value.Get<FVector2D>();
// pass the axis values to the move input
DoMove(MovementVector.X, MovementVector.Y);
}
void ASurviverPlayer::LookInput(const FInputActionValue& Value) {
// get the Vector2D look axis
FVector2D LookAxisVector = Value.Get<FVector2D>();
// pass the axis values to the aim input
DoAim(LookAxisVector.X, LookAxisVector.Y);
}
void ASurviverPlayer::DoAim(float Yaw, float Pitch) {
if (GetController()) {
// pass the rotation inputs
AddControllerYawInput(Yaw);
AddControllerPitchInput(Pitch);
}
}
void ASurviverPlayer::DoMove(float Right, float Forward) {
if (GetController()) {
// pass the move inputs
AddMovementInput(GetActorRightVector(), Right);
AddMovementInput(GetActorForwardVector(), Forward);
}
}
void ASurviverPlayer::DoJumpStart() {
// pass Jump to the character
Jump();
}
void ASurviverPlayer::DoJumpEnd() {
// pass StopJumping to the character
StopJumping();
}
/**
* @brief 接收伤害的实现。
* @param DamageAmount 伤害量。
* @param InstigatorActor 造成伤害的Actor。
*/
void ASurviverPlayer::ReceiveDamage(float DamageAmount, AActor* InstigatorActor) {
if (HealthComponent) {
// 通过生命组件处理伤害
HealthComponent->HandleDamage(DamageAmount);
}
}
/**
* @brief 当玩家死亡时调用,打印日志。
* @todo 通知 GameMode 玩家已死亡,处理结算逻辑
*/
void ASurviverPlayer::OnPlayerDied() {
UE_LOG(LogTemp, Warning, TEXT("你死了"));
}

View File

@@ -0,0 +1,105 @@
// Copyright Majowaveon Games. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "DamageableInterface.h"
#include "GameFramework/Character.h"
#include "SurviverPlayer.generated.h"
struct FInputActionValue;
class UInputAction;
class UCameraComponent;
class UHealthComponent;
/**
* @class ASurviverPlayer
* @brief 玩家角色基类,具有网络同步的移动组件,实现了可受伤害接口。
* @ingroup Battle
*/
UCLASS()
class FIRSTPERSONDEMO_API ASurviverPlayer : public ACharacter, public IDamageableInterface {
GENERATED_BODY()
/** Pawn mesh: first person view */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
USkeletalMeshComponent* CharacterMesh;
/** First person camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UCameraComponent* CharacterCameraComponent;
protected:
/** Jump Input Action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* JumpAction;
/** Move Input Action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* MoveAction;
/** Look Input Action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* LookAction;
/** Mouse Look Input Action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* MouseLookAction;
/** Called from Input Actions for movement input */
void MoveInput(const FInputActionValue& Value);
/** Called from Input Actions for looking input */
void LookInput(const FInputActionValue& Value);
/** Handles aim inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoAim(float Yaw, float Pitch);
/** Handles move inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoMove(float Right, float Forward);
/** Handles jump start inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoJumpStart();
/** Handles jump end inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoJumpEnd();
/** Set up input action bindings */
virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override;
public:
/**
* @brief 构造函数,设置角色的默认属性。
*/
ASurviverPlayer();
protected:
/**
* @brief 管理角色生命值的组件。
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UHealthComponent* HealthComponent;
virtual void BeginPlay() override;
/**
* @brief 当玩家死亡时调用。
* @todo 添加死亡处理逻辑如UI弹窗等
*/
UFUNCTION()
void OnPlayerDied();
public:
virtual void Tick(float DeltaTime) override;
/**
* @brief 接收伤害的接口实现。
* @param DamageAmount 伤害量。
* @param InstigatorActor 造成伤害的Actor。
*/
virtual void ReceiveDamage(float DamageAmount, AActor* InstigatorActor) override;
};

View File

@@ -0,0 +1,63 @@
// Copyright Majowaveon Games. All Rights Reserved.
#include "SurviverPlayerController.h"
// Copyright Epic Games, Inc. All Rights Reserved.
#include "EnhancedInputSubsystems.h"
#include "Engine/LocalPlayer.h"
#include "InputMappingContext.h"
#include "FirstPersonDemoCameraManager.h"
#include "Blueprint/UserWidget.h"
#include "FirstPersonDemo.h"
#include "Widgets/Input/SVirtualJoystick.h"
ASurviverPlayerController::ASurviverPlayerController() {
// set the player camera manager class
PlayerCameraManagerClass = AFirstPersonDemoCameraManager::StaticClass();
}
void ASurviverPlayerController::BeginPlay() {
Super::BeginPlay();
// only spawn touch controls on local player controllers
if (ShouldUseTouchControls() && IsLocalPlayerController()) {
// spawn the mobile controls widget
MobileControlsWidget = CreateWidget<UUserWidget>(this, MobileControlsWidgetClass);
if (MobileControlsWidget) {
// add the controls to the player screen
MobileControlsWidget->AddToPlayerScreen(0);
}
else {
UE_LOG(LogFirstPersonDemo, Error, TEXT("Could not spawn mobile controls widget."));
}
}
}
void ASurviverPlayerController::SetupInputComponent() {
Super::SetupInputComponent();
// only add IMCs for local player controllers
if (IsLocalPlayerController()) {
// Add Input Mapping Context
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<
UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer())) {
for (UInputMappingContext* CurrentContext : DefaultMappingContexts) {
Subsystem->AddMappingContext(CurrentContext, 0);
}
// only add these IMCs if we're not using mobile touch input
if (!ShouldUseTouchControls()) {
for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts) {
Subsystem->AddMappingContext(CurrentContext, 0);
}
}
}
}
}
bool ASurviverPlayerController::ShouldUseTouchControls() const {
// are we on a mobile platform? Should we force touch?
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
}

View File

@@ -0,0 +1,51 @@
// Copyright Majowaveon Games. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SurviverPlayerController.generated.h"
class UInputMappingContext;
/**
*
*/
UCLASS()
class FIRSTPERSONDEMO_API ASurviverPlayerController : public APlayerController {
GENERATED_BODY()
public:
/** Constructor */
ASurviverPlayerController();
protected:
/** Input Mapping Contexts */
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
TArray<UInputMappingContext*> DefaultMappingContexts;
/** Input Mapping Contexts */
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
TArray<UInputMappingContext*> MobileExcludedMappingContexts;
/** Mobile controls widget to spawn */
UPROPERTY(EditAnywhere, Category="Input|Touch Controls")
TSubclassOf<UUserWidget> MobileControlsWidgetClass;
/** Pointer to the mobile controls widget */
UPROPERTY()
TObjectPtr<UUserWidget> MobileControlsWidget;
/** If true, the player will use UMG touch controls even if not playing on mobile platforms */
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
bool bForceTouchControls = false;
/** Gameplay initialization */
virtual void BeginPlay() override;
/** Input mapping context setup */
virtual void SetupInputComponent() override;
/** Returns true if the player should use UMG touch controls */
bool ShouldUseTouchControls() const;
};

View File

@@ -3,3 +3,8 @@
#include "Surviver_FPS/SurviverGameMode.h"
#include "Battle/SurviverPlayer.h"
ASurviverGameMode::ASurviverGameMode() {
DefaultPawnClass = ASurviverPlayer::StaticClass();
}

View File

@@ -18,4 +18,9 @@ class FIRSTPERSONDEMO_API ASurviverGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
/**
* @brief 构造函数,设置默认加入游戏时的玩家为 @ref ASurviverPlayer
*/
ASurviverGameMode();
};