Devlog 60: How to handle MMO style motion with UE and custom server over UDP

In this post, I show how I used UDP to handle motion updates from my game running in Unreal Engine 5. The UDP updates will be going to a custom java server for processing before sending the responses back to UE client.

In order to send and receive UDP messages from Unreal Engine, I used the Object Deliverer plugin, which I documented separately here:

UDP with free Object Deliverer plugin in UE + Java (unreal-mmo-dev.com)

The object deliverer (free asset) can be found here: https://www.unrealengine.com/marketplace/en-US/product/objectdeliverer

The custom java server can be found here: yazoo321/mmo_server_with_micronaut: Hobby project to create mmo server using Java Micronaut with JWT Auth, postgres and mongoDB support (github.com)

In this post, we focus a little more on how I processed the motion updates while leveraging UDP.

Custom UDP integration with Unreal Engine 5

Creating a component to handle communication

I first created a component which will be used to communicate with my Java server.

It’s called ‘Server Connector’ and its of an Object type.

Server connector component
Creating object blueprint

This Object blueprint contains logic for handling my UDP communication, see the previous post for the details.

Configure UDP comms

This component also exposes methods for receiving and sending UDP messages:

Send UDP Message
Receive UDP Message

Configuring UDP port for your game

Normally you would set a static port for your game. For example default UDP gameport for Unreal Engine is 7777.

UDP can only send messages to 1 application, so it will be a problem for us to test if we can only start 1 application with a given port.

In this case, UE will assign a new port for your other clients, for example, initial client will begin with 7777, the next will get 7778 and so on.

In order to handle this logic, you can check whether you’re able to bind to a particular port. There’s several ways to achieve this, simple is to try connect to a given port and if you get an error/exception of bind error, you know it’s taken and can try the next one.

The Object Deliverer plugin didn’t have the error handling capability to do this.

I was planning to check what gameport was assigned to the client and act accordingly with my UDP, however this functionality was only available in C++ and I wanted this sample to be fully in blueprints.

Long story short, I put a hacky solution in place which will assign a pretty random port to each game that I load:

assign a random port to our UDP listener

This initiation happens from the Game Instance. Game instance is best place for this because it’s always loaded and can be accessible from pretty much anywhere in our code. Usually developers will use it to store global variables which are commonly required from many places.

Creating connected actor component

We will synchronize our motion using an actor component. I will create a parent blueprint first which handles all of the communication aspects and then create components specific to processing the data.

So let’s create the Connected Component, of ‘Actor Component’ class.

Create actor component class

Now I will start configuring this component.

Event begin play for connected component

Prepare Variables

The ‘Prepare Variables’ function will depend on different use-cases. This particular component will be a parent for many types of components, such as player motion sync, mob motion sync, inventory sync, etc.

Prepare variables for connected component
Finishing prepare variables for connected component

The first and main thing that we need to do is get the UDP client which is available in our Game Instance. This is the Server Connector variable. I also fetch other parameters, such as the Map and Server Name.

Server name will be set at compile time for my server builds, so my player clients will not have this variable populated.

Prepare Actor ID

My prepare actor ID will populate the actor ID in this component as well as identify whether it’s the is Player Actor.

I also have an ‘Active Notification Widget’ which I populate here, incase components need to alert player with specific messages.

Register Event Listeners

Register Event Listeners

Now I want to register the event listeners. I create a Set of strings, which by default is empty in this component. It’s populated by its child classes.

You can see it’s binding to two event delegates, one is for the websocket and another for UDP connector. We will only focus on the UDP events.

Note that this is the same event dispatcher that you saw above; Call UDP Message Dispatcher.

The binding sits with Can Process Message? which will activate the process message if required.

Check whether component can process message

I configured my server to always send a Message Type along with the response. This indicates the domain of the response types and this is the part that components bind to essentially.

Note that you should use a Set and not an array because a Set datatype allows you to search with O(1) complexity.

The process message here is going to be overridden in the child function. This class acts as a template function.

Process message acts as a template

Creating Player Motion Sync component

Create Player Motion Sync component

Create a new blueprint which will be used to Sync the Players motion. When choosing the parent class, select the Connected Component Base that we just created as it will handle the base comms.

This component will not be listening to any updates as it will only push data.

No listening update types defined

We will populate it in components which will be listening to updates.

Event begin play – start sync

Event begin play - sync actor motion

This function will be very similar to sync other actor motion, main difference is that the message type will be different. So there’s additional opportunities to refactor here.

Let’s see what we’re doing though;

  • When this component is loaded, start a loop event which handles actor sync
  • On each event, get actors motion
  • Check if its the same as last processed motion
  • If not the same, push update to server

Let’s see these functions now.

Get Actor Motion

Get actor motion

The key functions are:

  • Get Actor Location
  • Get Actor RTotation
  • Get Actor Velocity

I just take these data points and push it to a structure.

This function is part of a utility blueprint (function library) so that it can be reused in other places.

Check motion equals

Check motion is equal

Weirdly, there’s no function in blueprint which allows me to check equality of two structures. So I effectively have to check each parameter inside the structure.

This is also part of the utilities class.

Update Player Motion

In this function we populate the message that we need to send to the server.

Populate message to send to server

Note how similar this is to previous function, which utilized Websocket instead of UDP.

This is where I mentioned before, we can build scalable parts and switch our communication methods as we improve our system through iterative design.

The send UDP message is the function we built earlier, part of Server Connector.

Referencing Send UDP message

Sync nearby players

In order to sync nearby players, I have another component. It’s called Proxy Player Sync.

This component is created in same way as Player Motion Sync – set the Connected Component Base as a parent blueprint class.

Proxy Player Sync listening to updates

Next, you can see that in Class Defaults I configured some Listening Update Types. These are the message types that this component will be responsible to process.

This component listens to updates for:

  • player motion updates
  • player appearance updates
  • removing player updates

The event graph is empty, but we define some functions to handle the processing, note the override of ProcessMessage.

Event graph for player proxy sync

Process Message – override

If you look back at the parent class, the Process Message function was left empty, but its called from Can Process Message?

Here it is for reference:

Check the Can Process Message? functionality

We can see that the base class doesn’t actually process the message, but these child classes do.

In this case, this component expects 3 types of messages, so we have a switch statement to process the expected message.

Process message based on update type

Process motion update

This function will update the actors representing other players on our clients machines.

Process motion update part 1
Process motion update part 2

This function receives motion updates from nearby players. The custom server can send multiple updates in 1 message, though at the moment it only updates 1 at a time.

The data exists in Player Motion map, containing 1 item, but we still need to iterate over it, using the for each loop, using the map key(s).

For reference, the player motion struct:

Player motion struct

and the motion struct:

motion struct

I am not using all of these parameters so I may want to reduce the load here.

You can see in the blueprint I have a variable named ‘SynchedPlayers‘ So this component is tracking the nearby players.

Synched Players variable

And the Character Data struct:

Character Data struct

Handle new Player Motion

Since we’re tracking players already we can easily determine whether the motion is for a new player or existing player.

Handle New Player motion

As you can see, the main thing we need to do is create a new actor which is going to represent the player and I will add it to the Synched Players variable. At the beginning its set to hidden as we also need to receive an update for the appearance information.

Process Appearance

Before we go into handle existing player, let’s check how we handle appearance.

Handle character appearance

So, currently my server cannot guarantee whether the appearance information is received before or after the motion update, this causes a bit of a problem – there’s a workaround (which is not perfect).

Let’s first go through the scenario when the motion was received and new actor was created, through the Handle New Player Motion function above.

We’re able to find the Synched Player and we update the Account Character variable, which holds the AppearanceInfo.

Handle Existing Player Appearance

I will not go into detail about how the appearance is resolved (there’s a separate post on that). But its effectively a Map of keys to values, representing which component should look like what.

You can see after resolving the appearance, the Set Actor Hidden In Game is set to false.

When we’re processing appearance and we didn’t process the motion yet, the actor doesn’t exist yet.

Rather than creating one with random location, I set a field for ‘appearance sync required’ – which basically means I populate the Account Character field but it’s not yet assigned to the character.

It will be processed when we receive a motion update, which we know has to come soon.

Handle Existing Player Motion

Handle existing player motion part 1

This function will process the existing player motion. It means we have a record in our Synched Players variable. We first want to check whether Appearance Sync Required.

If required, we will re-use existing functionality, to Handle New Player Motion. This will create the actor for us and all we need to do is Handle Existing Player Appearance, because now the actor exists. Finally we call this very same function again, but in the subsequent call, the Appearance Sync Required will be false.

Handle Existing Player Motion - appearance sync not required

This handles the vanilla motion update, it essentially updates the Target Location, rotation and velocity. I have a Motion Smoother component which deals with the smoothing aspects and there’s a separate post on that too.

What’s next

So that’s it for player motion synching. I am now able to use the same structure to create other components.

Components to handle sync

The next one I plan to bring to UDP is the mob motion sync. The others can remain on Websocket for now, but you saw how simple it can be to switch them in UE. There may be some dependencies in my custom server, such as ones relying on websocket local sessions. However I transferred most dependencies to use Redis cache which is shared.