Gah - a solution with more questions. – EntropicLqd

Legacy:Kiff

From Unreal Wiki, The Unreal Engine Documentation Site
Jump to: navigation, search

Under Construction...

Replication Example: The ESTransport's compartment.

I'm still pretty new to unreal coding (and modding), but I think I finally understand replication well enough to accomplish what I needed to do here and pass on what I've learned. That said, I'm still learning so please give feedback.

I have a custom transport vehicle (thanks to showNOmercy for the model) that has an "animated" storage compartment. The compartment is a box that can be lowered and then open its door. Currently the animation is done purely in code. This isn't the best way, but I opted to take this aproach since I simply wanted to know how to do this and I also didn't want to bug the modeler with creating the animations. What does this have to do with replication? Keep reading...

Animating the compartment

The compartment's animation sequence is broken into six (0 - 5) parts or "states" in code (not unreal script states).

  • 0: Fully closed state: The box is in its up position and the door is closed
  • 1: Lowering: The box is going from its up position to its low position. The door is still closed.
  • 2: Low position, opening the door.
  • 3: Low position, door is fully opened. This is a stopping point for the animation.
  • 4: Low position, closing the door
  • 5: Raising: The door has been closed and now the box is returning to its up position.

If this sounds complicated it's really not. Don't worry too much about all of the states. Just keep in mind that the box can be open, closed or in a transition from one to the other.

Here is an ingame video

A struct is used to represent the current state and for replicating it when it has changed

struct BoxState
{
	var float 	DoorRotation;
	var float 	BoxHeight;
	var int		Mode;
};

Keep in mind that we want the client and server to animate their own copies of the compartment. The struct "CurrentBoxState" could just be set as reliable, but then the server would be doing all of the animating and would spam all of the small incremental position updates over the network.

Triggering the animation

The driver opens or closes the compartment by right-clicking. If the door is currently open, then clicking again will start closing it. Also, if the door is in motion, clicking will cause it to reverse and "go back".

The driver's right-click causes "VehicleFire" to get called on the server with bWasAltFire set to true. Now the code will check the current state and change it accordingly:

function VehicleFire(bool bWasAltFire)
{
	if (!bWasAltFire)
		return;
 
	// to initiate opening the box, we must be "parked"
	if (0 == CurrentBoxState.Mode)
		if (VSize(Velocity) > 1.5)
		{	 
			if (PlayerController(Controller) != None)
				PlayerController(Controller).ClientPlaySound(CantOpenDoorSound);
 
			return;
		}
 
	switch(CurrentBoxState.Mode)
	{
		case 0:	CurrentBoxState.Mode = 1;
				PlaySound(LowerBoxSound,,2.5*TransientSoundVolume);
				Enable('Tick');
				break;
 
		case 1:	CurrentBoxState.Mode = 5;
				break;
 
		case 2:	CurrentBoxState.Mode = 4;
				break;
 
		case 3:	CurrentBoxState.Mode = 4;
				PlaySound(OpenDoorSound,,3*TransientSoundVolume,,,0.5);
				Enable('Tick');
				break;
 
		case 4:	CurrentBoxState.Mode = 2;
				break;
 
		case 5:	CurrentBoxState.Mode = 1;
				break;
 
	}
 
	NewBoxState = CurrentBoxState;
	NewBoxState.Mode  = CurrentBoxState.Mode; // struct replication quirk
	BoxUpdateTime = Level.TimeSeconds;
	NetUpdateTime = Level.TimeSeconds - 1;
 
}

The last four lines are where replication comes into play. When the state changes on the server, it "tells" the clients by changing the replicated variables NewBoxState and BoxUpdateTime. BoxUpdateTime is used as a counter of sorts to inform the client when it happened and gives the client a way to know that it's a new event. The very last line is a commonly used method to tell the server to start replicating as soon as possible, but note that NetUpdateTime is not a variable defined in my code.

Kiff: I think that "BoxUpdateTime" might need to be added to the struct to make sure it arrives at the same time.

Kiff: Now that I think about it, I don't even think the update time is needed. It was needed for an older method I was using, but I'm pretty sure that now just the state (.Mode) can just be checked for a change.

Here is the replication block:

replication
{
    reliable if (Role == ROLE_Authority)
        BoxUpdateTime, NewBoxState;
}

When a client receives new values for replicated variables (and bNetNotify=True) the PostNetReceive event is fired.

simulated event PostNetReceive()
{
	if(BoxUpdateTime > LastBoxUpdateTime)
	{
		LastBoxUpdateTime = BoxUpdateTime;
		CurrentBoxState =  NewBoxState;
		Enable('Tick');
	}
}

If the variable BoxUpdateTime is newer (greater) than the last one we saved (with LastBoxUpdateTime), we then know the NewBoxState struct has fresh data. Now we can update our local CurrentBoxState with the newly replicated NewBoxState and then start the animation.

Now we're able to use PostNetReceive as a remote function call. Unlike replicated functions that only reach a single (owner) client, this will reach all clients. Any number of function calls can be carried out this way as long as we have unique replicated variables to inspect for each one.

To wrap things up, here's the function that does the "animation". It takes a time delta as an arguement when called from the Tick event.

simulated function UpdateBoxPosition(float DT)
{
	local vector disp;
	local rotator doorRot;
 
	switch(CurrentBoxState.Mode)
	{
		case 0:	Disable('Tick'); // nothing moves in this state
				break;
 
		case 1:	CurrentBoxState.BoxHeight -= BoxMoveRate * DT;
				if (CurrentBoxState.BoxHeight < LoweredBoxHeight)
				{
					CurrentBoxState.BoxHeight = LoweredBoxHeight;
					CurrentBoxState.Mode = 2;
					PlaySound(OpenDoorSound,,3*TransientSoundVolume,,,0.5);
				}
				break;
 
		case 2:	CurrentBoxState.DoorRotation -= DoorRotRate * DT;
				if (CurrentBoxState.DoorRotation <= LoweredDoorRot)
				{
					CurrentBoxState.DoorRotation = LoweredDoorRot;
					CurrentBoxState.Mode = 3;
				}
				break;
 
		case 3:	Disable('Tick'); // this is a "stopped" state 
				break;
 
		case 4:	CurrentBoxState.DoorRotation += DoorRotRate * DT;
				if (CurrentBoxState.DoorRotation >= 0)
				{
					CurrentBoxState.DoorRotation = 0;
					CurrentBoxState.Mode = 5;
					PlaySound(LowerBoxSound,,2.5*TransientSoundVolume);
				}
				break;
 
		case 5:	CurrentBoxState.BoxHeight += BoxMoveRate * DT;
				if (CurrentBoxState.BoxHeight >= 0.0)
				{
					CurrentBoxState.BoxHeight = 0.0;
					CurrentBoxState.Mode = 0;
				}
	}
 
	if (0 == CurrentBoxState.Mode)
		SetPhysics(PHYS_Karma); // we can resume normal driving now
	else
	    SetPhysics(PHYS_None); // temporary hack to "park" the vehicle
 
	disp.Z = CurrentBoxState.BoxHeight;
	SetBoneLocation('LiftForks', disp,1.0);
	SetBoneLocation('StorageBox', disp,1.0);
	SetBoneLocation('StorageBox_Door', disp,1.0);
	doorRot.Pitch = CurrentBoxState.DoorRotation;
	SetBoneRotation('StorageBox_Door', doorRot,0, 1.0);
 
}