Note that a lot of ‘movement’ has been updated to use Websockets in chapter 26 and 27.
https://unreal-mmo-dev.com/2022/10/26/26-unreal-engine-synchronize-motion-with-a-custom-server/
However, if you’re looking for simple HTTP guide on achieving this, continue reading! 🙂
In the previous step, we’ve configured a basic Java Micronaut application. This will be used as a building block for our development.
Now, I will clean the packages and create some basic classes. Note that we will likely be refactoring few times as we progress, it will only make sense at the time of writing and you can make your own decisions about locations for logic within the code.
Also note that some credit goes to this YouTube video tutorial, it’s based in C# and I’ve adapted the code to fit my Java needs.
To get started with, here’s a small overview of what we will be building
Defined Java Classes
Motion
This is a ‘Motion’ DTO (Data Transfer Object). It’s a simple java object which will hold information about position, rotation and velocity. I’ve put it under common.dto
package as I expect other game objects will share similar/same motion parameters.
The code for it:
package server.common.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Motion {
// Position
Integer x;
Integer y;
Integer z;
// Rotation
Integer pitch;
Integer roll;
Integer yaw;
// Velocity
Integer vx;
Integer vy;
Integer vz;
}
Note* We will add libraries for Lombok dependencies a bit later
PlayerMotion
This class is another DTO, it simply holds information about the player (player name for now) and their Motion component.
Here’s the code:
package server.player.motion.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import server.common.dto.Motion;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerMotion {
String playerName;
Motion motion;
}
PlayerMotionList
This is a DTO for just a list of PlayerMotions
Why have these DTOs? They greatly help with readability in due course – they are also automatically serialized and de-serialized in JSON where we need them.
Service and Controller
PlayerMotionService
This class deals with actually processing player motion. For now, its very simple. We just updatePlayerState
and getPlayersNearMe
. Here’s the code that we use for it:
package server.player.motion.service;
import server.player.motion.dto.PlayerMotion;
import server.player.motion.dto.PlayerMotionList;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Singleton
public class PlayerMotionService {
Map<String, PlayerMotion> playerMotions = new HashMap<>();
public void updatePlayerState(PlayerMotion request) {
String playerName = request.getPlayerName();
if (playerMotions.containsKey(playerName)) {
playerMotions.get(playerName).setMotion(request.getMotion());
} else {
playerMotions.put(request.getPlayerName(), request);
}
}
public PlayerMotionList getPlayersNearMe(String playerName) {
PlayerMotionList playerMotionList = new PlayerMotionList();
List<PlayerMotion> playersNearMe = new ArrayList<>();
playerMotions.forEach((name, motion) -> {
if (!motion.getPlayerName().equals(playerName)) {
// add more filters, e.g. by distance
// consider caching
playersNearMe.add(motion);
}
});
playerMotionList.setPlayerMotionList(playersNearMe);
return playerMotionList;
}
}
As you can see, it’s very simple at the moment, for updating state:
- when we update state, we check if player motion exists
- if it exists, we update their existing motion state
- if it does not exist we insert a new motion to track
For getting players near our player:
- iterate through the list of players
- if its not current player, add them to the list, with their motions
- Note this is over simplified, we’d need to add lots of additional filters such as by distance etc.
PlayerController
The controller is responsible for receiving the web requests and sending them to right service for processing and responding back.
package server.player;
import io.micronaut.http.annotation.*;
import server.player.motion.dto.PlayerMotion;
import server.player.motion.dto.PlayerMotionList;
import server.player.motion.service.PlayerMotionService;
import javax.inject.Inject;
@Controller("/player")
public class PlayerController {
@Inject
PlayerMotionService playerService;
@Post("/update-motion")
public PlayerMotionList updatePlayerLocation(@Body PlayerMotion playerMotionRequest) {
playerService.updatePlayerState(playerMotionRequest);
return playerService.getPlayersNearMe(playerMotionRequest.getPlayerName());
}
}
We can see that the controller is @Controller("/player")
and the function @Post("/update-motion")
This implies that our endpoint for this function resolves to: /player/update-motion
We will test this with Postman shortly.
Additional Micronaut/Java configuration
Let’s go to your application.yml
file and add the server port lines to it, it should now look like this:
micronaut:
application:
name: mmo_server
server:
port: 8081
This means if you run your service locally, the url for update motion will be:
http://localhost:8081/player/update-motion
Additional Gradle dependencies
We’ve referenced Lombok several times in code, it greatly improves readability and reduces number of lines by injecting constructors and getters/setters.
We’re also using micronaut and we’d need additional libraries to inject our services (like Autowire in SpringBoot).
so you’d want to add the following extras in your dependencies:
// Lombok:
compileOnly 'org.projectlombok:lombok:1.18.16'
annotationProcessor 'org.projectlombok:lombok:1.18.16'
// Micronaut inject
annotationProcessor(platform("io.micronaut:micronaut-bom:2.0.2"))
annotationProcessor("io.micronaut:micronaut-inject-java")
implementation(platform("io.micronaut:micronaut-bom:2.0.2"))
implementation("io.micronaut:micronaut-inject")
Overall it should look similar to this:
plugins {
id("com.github.johnrengelman.shadow") version "6.1.0"
id("io.micronaut.application") version "1.4.2"
}
version = "0.1"
group = "mmo_server"
repositories {
mavenCentral()
}
micronaut {
runtime("netty")
testRuntime("junit5")
processing {
incremental(true)
annotations("mmo_server.*")
}
}
dependencies {
implementation("io.micronaut:micronaut-http-client")
implementation("io.micronaut:micronaut-runtime")
implementation("io.micronaut:micronaut-validation")
runtimeOnly("ch.qos.logback:logback-classic")
// Lombok:
compileOnly 'org.projectlombok:lombok:1.18.16'
annotationProcessor 'org.projectlombok:lombok:1.18.16'
// Micronaut inject
annotationProcessor(platform("io.micronaut:micronaut-bom:2.0.2"))
annotationProcessor("io.micronaut:micronaut-inject-java")
implementation(platform("io.micronaut:micronaut-bom:2.0.2"))
implementation("io.micronaut:micronaut-inject")
}
application {
mainClass.set("mmo_server.Application")
}
java {
sourceCompatibility = JavaVersion.toVersion("1.8")
targetCompatibility = JavaVersion.toVersion("1.8")
}
Also, if like me you’ve ported this project to Windows (as you’d be using Unreal Engine to test with and it works nicely in Windows) you may come across a similar error to me. A class not found exception on WindowsAnsiOutputStream
.
To fix this, just disable withJansi
using your search tool like so:
Ok cool, the server code is ready to be tested.
Run a basic test with Postman
Before jumping to Unreal Engine, just check to make sure its working as expected using Postman.
First, start your application (ideally inside intellij, right click Application
and hit Run) and make sure its running by checking the output
Double check that the port is set correctly: http://localhost:8081
if not, check the step where we modified application.yml
.
Now open Postman, make a POST
request to http://localhost:8081/player/update-motion
Add Headers
the following:
Content-Type: application/json
Accept: application/json
Make the payload body to be:
{
"playerName":"b",
"motion":{
"x":123,
"y":123,
"z":123,
"pitch":321,
"roll":321,
"yaw":321,
"vx":231,
"vy":231,
"vz":231
}
}
You should recognize this to be the form of PlayerMotion
DTO.
Send the request:
As you can see, the request was successful (200 OK) and the payload (players near me) is empty, as there’s no other registered player.
Let’s send the request again but now with another player name:
As you can see now, the response is different and contains a list of players which includes the player referenced in first request.
This is great, exactly what we need for this stage.
Next step is to link this in Unreal Engine code, but the post is getting rather large so will reference it as a new chapter.