2. Java + MMO: Simple steps to handle player movement

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.

2 Comments

  1. foatin

    Where is code of PlayerMotionList? I do not see here nor on github

    • Hey!
      I actually deprecated that around part 10/11 https://ylazarev.com/2021/10/28/11-create-mmo-server-handle-motion/
      But apologies the code wasn’t specified in this post!
      It would simply have been a class with `@Data` and `@AllArgsConstructor`
      and the body would have simply contained
      `List<PlayerMotion` playerMotionList`

      or very closely would have matched that

      What I've now done instead is merge the motion information as part of `Character`.
      The rationale behind is that you'd usually want more than just motion, e.g. character name, equipment, etc.
      so instead, currently you'd return
      `List accountCharacters;`
      and `Character` would include `Motion` information

Comments are closed