Hello all,
TL;DR Set Up Replication on the first of my character variables, suddenly client characters are not able to move. Server (Host) character can move just fine.
I didn’t realize how difficult it was going to be to convert a single-player game to a multiplayer game since I started producing one. So… basically, I’ve been working on a multiplayer game but treating it as a single player game in terms of code. I have a few core systems in already, but nothing really serious. I recently realized that almost nothing in my game will work as a multiplayer experience without setting up replication and RPCs, so I have been trying to modify my code to support my originally intended multiplayer functionality.
The first thing that I tackled in this was to make sure that all of my game play relevant projectiles are replicated to clients. To do this, I renamed the OnFire() function (I started this project from a C++ 1st Person Template) to ClientOnFire(), and moved the logic from this function into a ServerOnFire() function (which is called only on the server. This allowed all projectiles to be spawned on the server.
This was working all fine and good. So I decided to tackle some player variable replication next. I started with the Equipped_Ammo_Remaining variable. I successfully set this up to replicate (ie. It compiles successfully). However, upon testing the game, I now have a fairly serious problem. After making this change, my player controlled characters can no longer move on clients. The server character can move just fine. but no client characters can. When using a listen server, the server character moves properly, but the client characters cannot. When using a dedicated server, none of the characters (all on clients) can move.
I’m really stumped here. I am certain that the change to Equipped_Ammo_Remaining
(including the addition of “void AMonkeyNutsCharacter::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const” ) has caused my client characters to be unable to move, but I honestly have no idea why.
I’d appreciate any help with this that I can get. Thanks in advance for any response.
Code for my Character Class is shown below.
MonkeyNutsCharacter.h
// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "MonkeyNuts.h"
#include "UnrealNetwork.h"
#include "WeaponTypes.h"
#include "MNPickup.h"
#include "MNWeaponPickup.h"
#include "MNCharacterMovementComponent.h"
#include "MonkeyNutsProjectile.h"
#include "MonkeyNutsCharacter.generated.h"
UCLASS(config=Game)
class AMonkeyNutsCharacter : public ACharacter
{
GENERATED_UCLASS_BODY()
//////////***************Component Properties*******************//////////
…//Insert Component initialization stuff
//////////******************************************************//////////
//***Turning Variables**
/** Base turn rate, in deg/sec. Other scaling may affect final turn rate. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
float BaseTurnRate;
/** Base look up/down rate, in deg/sec. Other scaling may affect final rate. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
float BaseLookUpRate;
//***Weapon and Projectile**
/** Gun muzzle's offset from the characters location */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
FVector GunOffset;
/** Projectile class to spawn */
UPROPERTY(EditDefaultsOnly, Category = Projectile)
TSubclassOf<class AMonkeyNutsProjectile> ProjectileClass;
/** Sound to play each time we fire */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
USoundBase* FireSound;
/** AnimMontage to play each time we fire */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
UAnimMontage* FireAnimation;
//**********Function Prototypes**********
//////////***************Return Player Var Methods**************//////////
///Insert Get Private Variable Functions Here
//////////******************************************************//////////
//TICK FUNCTION Override for the player
UFUNCTION()
void Tick(float DeltaSeconds) override;
//Allows the character source to receive OnActorBeginOverlap Events
virtual void ReceiveActorBeginOverlap(AActor* OtherActor) override;
//Allows the character source to receive OnActorB=EndOverlap Events
virtual void ReceiveActorEndOverlap(AActor* OtherActor) override;
//Allows the character to recieve OnBeginPlayEvents
virtual void ReceiveBeginPlay() override;
UFUNCTION(BlueprintCallable, Category = PlayerStatus)
//Allows the character to check for interactable objects when overlapping other objects
void CheckForInteractables(int32 OnorOff);
//Returns what the character is currently looking at
UFUNCTION(BlueprintCallable, Category = PlayerStatus)
AActor* GetLookingAt();
//Returns the Closest Interactable to the character
UFUNCTION(BlueprintCallable, Category = PlayerStatus)
AMNInteractable* GetClosestInteractable();
UFUNCTION()
void RunATrace();
UPROPERTY()
AActor* InteractableInRange;
void PickUpWeapon(AMNWeaponPickup* WeaponPU);
private:
//////////***************Primary Player Variables***************//////////
//Character Health
UPROPERTY(EditAnywhere, Instanced, Category = PlayerStatus)
int32 PlayerHealth;
//The Character's Currently Equipped Weapon
UPROPERTY(EditAnywhere, Instanced, Category = PlayerEquipment)
TEnumAsByte<EWeaponTypes::Type> EquippedWeapon;
//The Character's Currently Equipped Backup Weapon
UPROPERTY(EditAnywhere, Instanced, Category = PlayerEquipment)
TEnumAsByte<EWeaponTypes::Type> BackupWeapon;
//Stores which carried weapon is currently equipped. 1 for Primary, 2 for Secondary.
UPROPERTY(EditAnywhere, Instanced, Category = PlayerEquipment)
int32 CurrentWeapon;
//////////******************************************************//////////
//The Closest Interactable to The Character
AMNInteractable* ClosestInteractable;
//The What the Character Is Currently looking at.
AActor* ThingImLookingAt;
//////////***************Player Ammunition Variables***************//////////
//Total Amount of Ammo Stockpiled for Current Weapon
UPROPERTY(EditAnywhere, Replicated, Instanced, Category = PlayerEquipment)
int32 Equipped_Ammo_Remaining;
///INSERT OTHER Variable Properties similar to Equipped_Ammo_Remaining, bbut these are not replicated.
//////////******************************************************//////////
protected:
void Interact();
void SwitchWeapon();
/** Handler for a touch input beginning. */
void TouchStarted(const ETouchIndex::Type FingerIndex, const FVector Location);
/** Fires a projectile. */
void ClientOnFire();
//Server Driven Fire Function
UFUNCTION(Reliable, Server, WithValidation)
void ServerOnFire();
/** Handles moving forward/backward */
void ClientMoveForward(float Value);
UFUNCTION(Reliable, Server, WithValidation)
void ServerMoveForward(float Value);
/** Handles stafing movement, left and right */
void ClientMoveRight(float Value);
void ServerMoveRight(float Value);
/*
* Called via input to turn at a given rate.
* @param Rate This is a normalized rate, i.e. 1.0 means 100% of desired turn rate
*/
void TurnAtRate(float Rate);
/**
* Called via input to turn look up/down at a given rate.
* @param Rate This is a normalized rate, i.e. 1.0 means 100% of desired turn rate
*/
void LookUpAtRate(float Rate);
// APawn interface
virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;
// End of APawn interface
};
MonkeyNutsCharacter.cpp
// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.
#include "MonkeyNuts.h"
#include "UnrealNetwork.h"
#include "WeaponTypes.h"
#include "MNWeaponPickup.h"
#include "MonkeyNutsCharacter.h"
#include "Math.h"
#include "MNWeaponInfo.h"
//////////////////////////////////////////////////////////////////////////
// AMonkeyNutsCharacter
AMonkeyNutsCharacter::AMonkeyNutsCharacter(const class FPostConstructInitializeProperties& PCIP)
: Super(PCIP.SetDefaultSubobjectClass<UMNCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
{
//***Basic Properties***
bReplicates = true;
bOnlyRelevantToOwner = false;
bReplicateInstigator = true;
bReplicateMovement = true;
// set our turn rates for input
BaseTurnRate = 45.f;
BaseLookUpRate = 45.f;
/** CharacterMovement component used by walking/running/flying avatars not using rigid body physics */
UPROPERTY(Category = Character, VisibleAnywhere, BlueprintReadOnly)
TSubobjectPtr<class UMNCharacterMovementComponent> CharacterMovement;
// Default offset from the character location for projectiles to spawn
GunOffset = FVector(100.0f, 30.0f, 10.0f);
//***Component Properties***
// Set size for collision capsule
CapsuleComponent->InitCapsuleSize(42.f, 96.0f);
// Create a CameraComponent
FirstPersonCameraComponent = PCIP.CreateDefaultSubobject<UCameraComponent>(this, TEXT("FirstPersonCamera"));
FirstPersonCameraComponent->AttachParent = CapsuleComponent;
FirstPersonCameraComponent->RelativeLocation = FVector(0, 0, 128.f); // Position the camera
//RootComponent = FirstPersonCameraComponent;
// Create a mesh component that will be used when being viewed from a '1st person' view (when controlling this pawn)
Mesh1P = PCIP.CreateDefaultSubobject<USkeletalMeshComponent>(this, TEXT("CharacterMesh1P"));
Mesh1P->SetOnlyOwnerSee(true); // only the owning player will see this mesh
Mesh1P->AttachParent = FirstPersonCameraComponent;
//Mesh1P->AttachTo(FirstPersonCameraComponent);
//Mesh1P->RelativeLocation = FVector(0.f, 0.f, -150.f);
Mesh1P->bCastDynamicShadow = false;
Mesh1P->CastShadow = false;
//Mesh1P->AlwaysLoadOnClient;
//Create a mesh component that will be used when being veiwed by other players.
Mesh3P = PCIP.CreateDefaultSubobject<USkeletalMeshComponent>(this, TEXT("CharacterMesh3P"));
Mesh3P->SetOwnerNoSee(true);
Mesh3P->SetOnlyOwnerSee(false);
Mesh3P->AttachParent = FirstPersonCameraComponent;
//Mesh3P->AttachTo(FirstPersonCameraComponent);
//Mesh3P->RelativeLocation = FVector(0.f, 0.f, -200.f);
Mesh3P->bCastDynamicShadow = true;
Mesh3P->CastShadow = true;
// Note: The ProjectileClass and the skeletal mesh/anim blueprints for Mesh1P are set in the
// derived blueprint asset named MyCharacter (to avoid direct content references in C++)
//***Public Player Variables***
PlayerHealth = 100;
EquippedWeapon = EWeaponTypes::Type::Pistol;
BackupWeapon = EWeaponTypes::Type::Unarmed;
Equipped_Ammo_Remaining = 100000;
Backup_Ammo_Remaining = 100000;
Equipped_AmmoClip_Remaining = 100000;
Equipped_AmmoClip_Max = 100000;
Backup_AmmoClip_Remaining = 100000;
Backup_AmmoClip_Max = 100000;
Grenade_Ammo = 2;
}
//////////////////////////////////////////////////////////////////////////
// Input
void AMonkeyNutsCharacter::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
// set up gameplay key bindings
check(InputComponent);
InputComponent->BindAction("Jump", IE_Pressed, this, &AMonkeyNutsCharacter::Jump);
InputComponent->BindAction("Interact", IE_Pressed, this, &AMonkeyNutsCharacter::Interact);
InputComponent->BindAction("SwitchWeapons", IE_Pressed, this, &AMonkeyNutsCharacter::SwitchWeapon);
InputComponent->BindAction("Fire", IE_Pressed, this, &AMonkeyNutsCharacter::ClientOnFire);
InputComponent->BindTouch(EInputEvent::IE_Pressed, this, &AMonkeyNutsCharacter::TouchStarted);
InputComponent->BindAxis("MoveForward", this, &AMonkeyNutsCharacter::ClientMoveForward);
InputComponent->BindAxis("MoveRight", this, &AMonkeyNutsCharacter::ClientMoveRight);
// We have 2 versions of the rotation bindings to handle different kinds of devices differently
// "turn" handles devices that provide an absolute delta, such as a mouse.
// "turnrate" is for devices that we choose to treat as a rate of change, such as an analog joystick
InputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
InputComponent->BindAxis("TurnRate", this, &AMonkeyNutsCharacter::TurnAtRate);
InputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
InputComponent->BindAxis("LookUpRate", this, &AMonkeyNutsCharacter::LookUpAtRate);
}
void AMonkeyNutsCharacter::ClientOnFire()
{
if (Role <= ROLE_Authority){
AMonkeyNutsCharacter::ServerOnFire();
}
}
void AMonkeyNutsCharacter::ServerOnFire_Implementation(){
if (ProjectileClass != NULL)
{
const FRotator SpawnRotation = GetControlRotation();
// MuzzleOffset is in camera space, so transform it to world space before offsetting from the character location to find the final muzzle position
const FVector SpawnLocation = GetActorLocation() + SpawnRotation.RotateVector(GunOffset);
UWorld* const World = GetWorld();
if (World != NULL)
{
// spawn the projectile at the muzzle
World->SpawnActor<AMonkeyNutsProjectile>(ProjectileClass, SpawnLocation, SpawnRotation);
}
}
// try and play the sound if specified
if (FireSound != NULL)
{
UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation());
}
// try and play a firing animation if specified
if (FireAnimation != NULL)
{
// Get the animation object for the arms mesh
UAnimInstance* AnimInstance = Mesh1P->GetAnimInstance();
if (AnimInstance != NULL)
{
AnimInstance->Montage_Play(FireAnimation, 1.f);
}
}
}
bool AMonkeyNutsCharacter::ServerOnFire_Validate(){
return true;
}
void AMonkeyNutsCharacter::TouchStarted(const ETouchIndex::Type FingerIndex, const FVector Location)
{
// only fire for first finger down
if (FingerIndex == 0)
{
ClientOnFire();
}
}
void AMonkeyNutsCharacter::ClientMoveForward(float Value)
{
if (Role <= ROLE_Authority){
AMonkeyNutsCharacter::ServerMoveForward(Value);
}
else if (Role == ROLE_Authority){
if (Value != 0.0f){
// add movement in that direction
AddMovementInput(GetActorForwardVector(), Value);
}
}
else{
//Unhandled Exception
}
}
void AMonkeyNutsCharacter::ServerMoveForward_Implementation(float Value){
if (Value != 0.0f){
//UE_LOG(LogTemp, Warning, TEXT("ServerMoveForward_Implementation Called. Value: %n"), Value);
// add movement in that direction
AddMovementInput(GetActorForwardVector(), Value);
}
}
bool AMonkeyNutsCharacter::ServerMoveForward_Validate(float Value){
return true;
}
void AMonkeyNutsCharacter::ClientMoveRight(float Value)
{
if (Value != 0.0f)
{
// add movement in that direction
AddMovementInput(GetActorRightVector(), Value);
}
}
void AMonkeyNutsCharacter::TurnAtRate(float Rate)
{
// calculate delta for this frame from the rate information
AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds());
}
void AMonkeyNutsCharacter::LookUpAtRate(float Rate)
{
// calculate delta for this frame from the rate information
AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds());
}
//////////////////////////////////////////////////////////////////////////
///Tick
//Ammo Value Retrieval Functions
int32 AMonkeyNutsCharacter::GetEquippedAmmo_Remaining(){
return Equipped_Ammo_Remaining;
}
int32 AMonkeyNutsCharacter::GetEquippedAmmoClip_Remaining(){
return Equipped_AmmoClip_Remaining;
}
int32 AMonkeyNutsCharacter::GetEquippedAmmoClip_Max(){
return Equipped_AmmoClip_Max;
}
int32 AMonkeyNutsCharacter::GetBackupAmmo_Remaining(){
return Backup_Ammo_Remaining;
}
int32 AMonkeyNutsCharacter::GetBackupAmmoClip_Remaining(){
return Backup_AmmoClip_Remaining;
}
int32 AMonkeyNutsCharacter::GetBackupAmmoClip_Max(){
return Backup_AmmoClip_Max;
}
int32 AMonkeyNutsCharacter::GetGrenade_Ammo(){
return Grenade_Ammo;
}
//Other Functions
void AMonkeyNutsCharacter::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const{
DOREPLIFETIME(AMonkeyNutsCharacter, Equipped_Ammo_Remaining);
}