Эх сурвалжийг харах

Refactoring of Game Screen, Added Keybinds, added small verification on server side

jan 1 сар өмнө
parent
commit
cb42af0be8

+ 3 - 0
core/src/main/java/me/lethunderhawk/Main.java

@@ -6,12 +6,15 @@ import me.lethunderhawk.event.EventDispatcher;
 import me.lethunderhawk.event.ExampleEventListener;
 import me.lethunderhawk.network.packet.PacketRegistry;
 import me.lethunderhawk.screen.MenuScreen;
+import me.lethunderhawk.settings.KeybindSettings;
 
 public class Main extends Game {
     public AssetManager assetManager = new AssetManager();
 
     @Override
     public void create() {
+        KeybindSettings.initialiseKeybinds();
+
         setScreen(new MenuScreen(this));
         EventDispatcher.addListener(new ExampleEventListener());
     }

+ 4 - 0
core/src/main/java/me/lethunderhawk/controller/CameraController.java

@@ -0,0 +1,4 @@
+package me.lethunderhawk.controller;
+
+public class CameraController {
+}

+ 74 - 0
core/src/main/java/me/lethunderhawk/controller/PlayerInputController.java

@@ -0,0 +1,74 @@
+package me.lethunderhawk.controller;
+
+import com.badlogic.gdx.Gdx;
+import me.lethunderhawk.event.EventDispatcher;
+import me.lethunderhawk.event.impl.PlayerMoveEvent;
+import me.lethunderhawk.network.GameClient;
+import me.lethunderhawk.network.packet.impl.PlayerPositionPacket;
+import me.lethunderhawk.settings.Keybind;
+import me.lethunderhawk.settings.KeybindSettings;
+import me.lethunderhawk.world.entity.player.Player;
+
+public class PlayerInputController {
+
+    public void update(
+        float delta,
+        Player player,
+        GameClient client
+    ) {
+
+        float moveX = 0f;
+        float moveY = 0f;
+
+        if (Gdx.input.isKeyPressed(
+            KeybindSettings.getKeybind(Keybind.UP))) {
+
+            moveY++;
+        }
+
+        if (Gdx.input.isKeyPressed(
+            KeybindSettings.getKeybind(Keybind.DOWN))) {
+
+            moveY--;
+        }
+
+        if (Gdx.input.isKeyPressed(
+            KeybindSettings.getKeybind(Keybind.LEFT))) {
+
+            moveX--;
+        }
+
+        if (Gdx.input.isKeyPressed(
+            KeybindSettings.getKeybind(Keybind.RIGHT))) {
+
+            moveX++;
+        }
+
+        float length =
+            (float)Math.sqrt(moveX * moveX + moveY * moveY);
+
+        if (length <= 0f) {
+            return;
+        }
+
+        moveX /= length;
+        moveY /= length;
+
+        player.x += moveX * player.getSpeed() * delta;
+        player.y += moveY * player.getSpeed() * delta;
+
+        client.send(new PlayerPositionPacket(
+            player.uuid,
+            player.x,
+            player.y
+        ));
+
+        EventDispatcher.getInstance().fire(
+            new PlayerMoveEvent(
+                player,
+                player.x,
+                player.y
+            )
+        );
+    }
+}

+ 0 - 2
core/src/main/java/me/lethunderhawk/network/GameClient.java

@@ -58,8 +58,6 @@ public class GameClient extends Listener implements EventListener {
         }
     }
 
-
-
     public void disconnect(){
         client.stop();
     }

+ 97 - 0
core/src/main/java/me/lethunderhawk/render/MapManager.java

@@ -0,0 +1,97 @@
+package me.lethunderhawk.render;
+
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.g2d.TextureRegion;
+import com.badlogic.gdx.maps.tiled.TiledMap;
+import com.badlogic.gdx.maps.tiled.TiledMapTile;
+import com.badlogic.gdx.maps.tiled.TiledMapTileSet;
+import com.badlogic.gdx.maps.tiled.TmxMapLoader;
+import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer;
+import com.badlogic.gdx.maps.tiled.tiles.StaticTiledMapTile;
+
+public class MapManager {
+
+    private static final int TILE_SIZE = 16;
+
+    private final TiledMap map;
+    private final OrthogonalTiledMapRenderer renderer;
+
+    private final Texture tilesetTexture;
+    private final TiledMapTileSet runtimeTileset;
+
+    public MapManager(String mapPath) {
+
+        tilesetTexture =
+            new Texture("TX/TX Tileset Grass.png");
+
+        runtimeTileset =
+            createRuntimeTileset();
+
+        map = new TmxMapLoader().load(mapPath);
+
+        map.getTileSets().addTileSet(runtimeTileset);
+
+        renderer =
+            new OrthogonalTiledMapRenderer(
+                map,
+                1f / TILE_SIZE
+            );
+    }
+
+    public void render(OrthographicCamera camera) {
+
+        renderer.setView(camera);
+
+        renderer.render();
+    }
+
+    public TextureRegion getRegion(int id) {
+
+        TiledMapTile tile =
+            runtimeTileset.getTile(id);
+
+        return tile == null
+            ? null
+            : tile.getTextureRegion();
+    }
+
+    private TiledMapTileSet createRuntimeTileset() {
+
+        TextureRegion[][] regions =
+            TextureRegion.split(
+                tilesetTexture,
+                TILE_SIZE,
+                TILE_SIZE
+            );
+
+        TiledMapTileSet tileset =
+            new TiledMapTileSet();
+
+        int id = 1;
+
+        for (TextureRegion[] row : regions) {
+
+            for (TextureRegion region : row) {
+
+                StaticTiledMapTile tile =
+                    new StaticTiledMapTile(region);
+
+                tile.setId(id);
+
+                tileset.putTile(id, tile);
+
+                id++;
+            }
+        }
+
+        return tileset;
+    }
+
+    public void dispose() {
+
+        renderer.dispose();
+        map.dispose();
+        tilesetTexture.dispose();
+    }
+}

+ 61 - 0
core/src/main/java/me/lethunderhawk/render/PlayerRenderer.java

@@ -0,0 +1,61 @@
+package me.lethunderhawk.render;
+
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+import me.lethunderhawk.network.GameClient;
+import me.lethunderhawk.world.entity.player.Player;
+
+public class PlayerRenderer {
+
+    private final ShapeRenderer renderer;
+
+    public PlayerRenderer(ShapeRenderer renderer) {
+        this.renderer = renderer;
+    }
+
+    public void render(
+        OrthographicCamera camera,
+        GameClient client
+    ) {
+
+        renderer.setProjectionMatrix(camera.combined);
+
+        renderer.begin(ShapeRenderer.ShapeType.Filled);
+
+        for (Player player : client.getOtherPlayers().values()) {
+            renderPlayer(player);
+        }
+
+        if (client.getPlayer() != null) {
+            renderPlayer(client.getPlayer());
+        }
+
+        renderer.end();
+    }
+
+    private void renderPlayer(Player player) {
+
+        if (player.avatar == null) {
+            return;
+        }
+
+        float cellSize = 2f;
+
+        for (int x = 0; x < 16; x++) {
+
+            for (int y = 0; y < 16; y++) {
+
+                renderer.setColor(
+                    player.avatar.getColor(x, y)
+                );
+
+                renderer.rect(
+                    player.x + x * cellSize,
+                    player.y + y * cellSize,
+                    cellSize,
+                    cellSize
+                );
+            }
+        }
+    }
+}

+ 69 - 0
core/src/main/java/me/lethunderhawk/render/WorldRenderer.java

@@ -0,0 +1,69 @@
+package me.lethunderhawk.render;
+
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+import com.badlogic.gdx.graphics.g2d.TextureRegion;
+import me.lethunderhawk.network.GameClient;
+import me.lethunderhawk.world.World;
+
+public class WorldRenderer {
+
+    private final SpriteBatch batch;
+    private final GameClient client;
+    private final MapManager mapManager;
+
+    public WorldRenderer(
+        SpriteBatch batch,
+        GameClient client,
+        MapManager mapManager
+    ) {
+
+        this.batch = batch;
+        this.client = client;
+        this.mapManager = mapManager;
+    }
+
+    public void render(OrthographicCamera camera) {
+
+        World world = client.getWorld();
+
+        if (world == null) {
+            return;
+        }
+
+        batch.setProjectionMatrix(camera.combined);
+
+        batch.begin();
+
+        float scale = world.getBlockScale();
+
+        for (int x = 0; x < world.getWidth(); x++) {
+
+            for (int y = 0; y < world.getHeight(); y++) {
+
+                int blockId = world.getBlock(x, y);
+
+                if (blockId == 0) {
+                    continue;
+                }
+
+                TextureRegion region =
+                    mapManager.getRegion(blockId);
+
+                if (region == null) {
+                    continue;
+                }
+
+                batch.draw(
+                    region,
+                    x * scale,
+                    y * scale,
+                    scale,
+                    scale
+                );
+            }
+        }
+
+        batch.end();
+    }
+}

+ 79 - 176
core/src/main/java/me/lethunderhawk/screen/GameScreen.java

@@ -2,268 +2,171 @@
 package me.lethunderhawk.screen;
 
 import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.Input;
 import com.badlogic.gdx.Screen;
-import com.badlogic.gdx.assets.AssetDescriptor;
-import com.badlogic.gdx.graphics.Color;
 import com.badlogic.gdx.graphics.GL20;
 import com.badlogic.gdx.graphics.OrthographicCamera;
-import com.badlogic.gdx.graphics.Texture;
 import com.badlogic.gdx.graphics.g2d.SpriteBatch;
-import com.badlogic.gdx.graphics.g2d.TextureRegion;
 import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
-import com.badlogic.gdx.maps.tiled.TiledMap;
-import com.badlogic.gdx.maps.tiled.TiledMapTile;
-import com.badlogic.gdx.maps.tiled.TiledMapTileSet;
-import com.badlogic.gdx.maps.tiled.TmxMapLoader;
-import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer;
-import com.badlogic.gdx.maps.tiled.tiles.StaticTiledMapTile;
 import com.badlogic.gdx.utils.viewport.ScreenViewport;
 import me.lethunderhawk.Main;
+import me.lethunderhawk.controller.PlayerInputController;
+import me.lethunderhawk.render.MapManager;
+import me.lethunderhawk.render.PlayerRenderer;
+import me.lethunderhawk.render.WorldRenderer;
 import me.lethunderhawk.world.entity.player.Player;
-import me.lethunderhawk.event.EventDispatcher;
-import me.lethunderhawk.event.impl.PlayerMoveEvent;
 import me.lethunderhawk.network.GameClient;
 import me.lethunderhawk.network.packet.impl.PlayerLogoutPacket;
-import me.lethunderhawk.network.packet.impl.PlayerPositionPacket;
-import me.lethunderhawk.world.World;
 import me.lethunderhawk.camera.CameraController;
 
 import java.io.IOException;
-import java.util.Map;
-import java.util.UUID;
 
 public class GameScreen implements Screen {
 
+    private final Main game;
+
     private final ShapeRenderer shapeRenderer;
     private final SpriteBatch spriteBatch;
+
     private final GameClient client;
-    private final Main mainGame;
+
     private OrthographicCamera camera;
     private ScreenViewport viewport;
-    private CameraController cameraController;
-    private World world;
-    private TiledMap tiledMap;
-    private OrthogonalTiledMapRenderer tiledMapRenderer;
-    private Texture tilesetTexture;
-    private TiledMapTileSet runtimeTileset; // will hold the runtime tileset you added
+
+    private final CameraController cameraController;
+    private final PlayerInputController inputController;
+
+    private MapManager mapManager;
+
+    private WorldRenderer worldRenderer;
+    private PlayerRenderer playerRenderer;
 
     public GameScreen(Main game, String ip) {
-        this.mainGame = game;
+
+        this.game = game;
+
         this.shapeRenderer = new ShapeRenderer();
         this.spriteBatch = new SpriteBatch();
+
         this.client = new GameClient();
+
         this.cameraController = new CameraController();
+        this.inputController = new PlayerInputController();
+
+        connect(ip);
+    }
+
+    private void connect(String ip) {
+
         try {
             client.connect(ip);
+
         } catch (IOException e) {
-            e.printStackTrace();
+
+            System.out.println("Couldn't connect to ip: " + ip);
+            System.out.println("Starting Singleplayer instead!");
         }
     }
 
     @Override
     public void show() {
-        // --- Load tileset texture and split into 16x16 tiles ---
-        Texture tilesetTexture = new Texture(Gdx.files.internal("TX/TX Tileset Grass.png")); // ensure file has extension
-        final int TILE_PX = 16;
-        TextureRegion[][] regions = TextureRegion.split(tilesetTexture, TILE_PX, TILE_PX);
-
-        // Create a TiledMapTileSet and put each tile in it (IDs start at 1)
-        TiledMapTileSet tileset = new TiledMapTileSet();
-        int id = 1;
-        for (int row = 0; row < regions.length; row++) {
-            for (int col = 0; col < regions[row].length; col++) {
-                TextureRegion region = regions[row][col];
-                if (region == null) continue;
-                StaticTiledMapTile tile = new StaticTiledMapTile(region);
-                tile.setId(id);
-                tileset.putTile(id, tile);
-                id++;
-            }
-        }
-        mainGame.assetManager.finishLoading();
-
-        // --- Load the Tiled map ---
-        TiledMap map = new TmxMapLoader().load("level/lobby.tmx");
 
-        // Add the runtime tileset to the map so its tiles can be referenced by layers/objects if needed
-        tileset.setName("runtime_grass");
-        map.getTileSets().addTileSet(tileset);
+        setupCamera();
 
-        this.tiledMap = map;
-        this.runtimeTileset = map.getTileSets().getTileSet(tileset.getName() /* tileset.getName() may be null */);
-
-        // fallback: if getTileSet by name fails, use first non-empty tileset:
-        if (this.runtimeTileset == null) {
-            for (TiledMapTileSet ts : map.getTileSets()) {
-                if (ts.size() > 0) { this.runtimeTileset = ts; break; }
-            }
-        }
+        mapManager = new MapManager("level/lobby.tmx");
 
-        // --- Setup renderer and camera ---
-        final float UNIT_SCALE = 1f / TILE_PX; // map tiles are 16px so 1/16f unit scale
-        OrthogonalTiledMapRenderer renderer = new OrthogonalTiledMapRenderer(map, UNIT_SCALE);
+        worldRenderer = new WorldRenderer(
+            spriteBatch,
+            client,
+            mapManager
+        );
 
-        // Determine map world size (in world units) and center camera on it
-        int mapTilesX = map.getProperties().get("width", Integer.class);
-        int mapTilesY = map.getProperties().get("height", Integer.class);
-        int mapTilePixelWidth = map.getProperties().get("tilewidth", Integer.class);
-        int mapTilePixelHeight = map.getProperties().get("tileheight", Integer.class);
+        playerRenderer = new PlayerRenderer(shapeRenderer);
+    }
 
-        float worldWidth = mapTilesX * mapTilePixelWidth * UNIT_SCALE;
-        float worldHeight = mapTilesY * mapTilePixelHeight * UNIT_SCALE;
+    private void setupCamera() {
 
         camera = new OrthographicCamera();
-        viewport = new ScreenViewport(camera);
-
-        // Set a reasonable viewport size: keep the screen viewport but ensure camera centers on map
-        camera.setToOrtho(false, viewport.getWorldWidth(), viewport.getWorldHeight());
-        camera.position.set(worldWidth / 2f, worldHeight / 2f, 0f);
-        camera.update();
 
-        renderer.setView(camera);
+        viewport = new ScreenViewport(camera);
 
-        // Store renderer / map / tileset as fields if you need to render/dispose later:
-        this.tiledMap = map;
-        this.tiledMapRenderer = renderer;
-        this.tilesetTexture = tilesetTexture; // keep a ref for disposal
+        camera.position.set(0, 0, 0);
 
+        camera.update();
     }
 
     @Override
     public void render(float delta) {
-        update(delta);
 
-        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+        update(delta);
 
-        // Render World with SpriteBatch
-        spriteBatch.setProjectionMatrix(camera.combined);
-        spriteBatch.begin();
-        renderWorld();
-        spriteBatch.end();
+        clearScreen();
 
-        // Render Players with ShapeRenderer
-        shapeRenderer.setProjectionMatrix(camera.combined);
-        shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
+        mapManager.render(camera);
 
-        for (Map.Entry<UUID, Player> entry : client.getOtherPlayers().entrySet()) {
-            renderPlayerGrid(entry.getValue());
-        }
-        if (client.getPlayer() != null) {
-            renderPlayerGrid(client.getPlayer());
-        }
+        worldRenderer.render(camera);
 
-        shapeRenderer.end();
+        playerRenderer.render(camera, client);
     }
 
-    private void renderWorld() {
-        World world = client.getWorld();
-        if (world == null || runtimeTileset == null) return;
-        float scale = world.getBlockScale(); // world units per tile
-
-        for (int x = 0; x < world.getWidth(); x++) {
-            for (int y = 0; y < world.getHeight(); y++) {
-                int blockId = world.getBlock(x, y);
-                if (blockId == 0) continue;
-
-                TextureRegion region = getRegionForBlock(blockId);
-                if (region != null) {
-                    spriteBatch.draw(region, x * scale, y * scale, scale, scale);
-                }
-            }
-        }
-    }
-
-    private TextureRegion getRegionForBlock(int blockId) {
-        // If your block IDs match the runtime tile IDs you assigned when creating the tileset:
-        TiledMapTile tile = runtimeTileset.getTile(blockId);
-        if (tile == null) return null;
+    private void update(float delta) {
 
-        // Most runtime tiles are StaticTiledMapTile; get the region:
-        if (tile.getTextureRegion() != null) {
-            return tile.getTextureRegion();
-        }
+        Player player = client.getPlayer();
 
-        // fallback for tiled tiles that wrap an atlas region:
-        if (tile instanceof StaticTiledMapTile) {
-            return ((StaticTiledMapTile) tile).getTextureRegion();
+        if (player == null) {
+            return;
         }
 
-        return null;
-    }
+        inputController.update(delta, player, client);
 
-    private void renderPlayerGrid(Player player) {
-        if (player.avatar == null) return;
-        float cellSize = 2f;
-        for (int x = 0; x < 16; x++) {
-            for (int y = 0; y < 16; y++) {
-                Color color = player.avatar.getColor(x, y);
-                shapeRenderer.setColor(color);
-                shapeRenderer.rect(player.x + x * cellSize, player.y + y * cellSize, cellSize, cellSize);
-            }
-        }
+        cameraController.update(delta, camera, player);
     }
 
-    private void update(float delta) {
-        float speed = 300f;
-        boolean moved = false;
-        Player currentPlayer = client.getPlayer();
-
-        if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) {
-            mainGame.setScreen(new MenuScreen(mainGame));
-            return;
-        }
+    private void clearScreen() {
 
-        if (currentPlayer != null) {
-            if (Gdx.input.isKeyPressed(Input.Keys.W)) { currentPlayer.y += speed * delta; moved = true; }
-            if (Gdx.input.isKeyPressed(Input.Keys.S)) { currentPlayer.y -= speed * delta; moved = true; }
-            if (Gdx.input.isKeyPressed(Input.Keys.A)) { currentPlayer.x -= speed * delta; moved = true; }
-            if (Gdx.input.isKeyPressed(Input.Keys.D)) { currentPlayer.x += speed * delta; moved = true; }
-
-            if (moved) {
-                PlayerPositionPacket packet = new PlayerPositionPacket(currentPlayer.uuid, currentPlayer.x, currentPlayer.y);
-                EventDispatcher.getInstance().fire(new PlayerMoveEvent(currentPlayer, currentPlayer.x, currentPlayer.y));
-                client.send(packet);
-            }
-        }
+        Gdx.gl.glClearColor(0, 0, 0, 1);
 
-        // Update camera dead-zone follow logic
-        if (currentPlayer != null) {
-            cameraController.update(delta, camera, currentPlayer);
-        }
+        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
     }
 
     @Override
     public void resize(int width, int height) {
-        if (viewport != null) {
-            viewport.update(width, height, true);
-            camera.update();
-        }
-    }
 
-    @Override
-    public void pause() {}
-
-    @Override
-    public void resume() {}
+        viewport.update(width, height, true);
+    }
 
     @Override
     public void hide() {
+
         logout();
     }
 
     @Override
     public void dispose() {
+
         logout();
+
         shapeRenderer.dispose();
+        spriteBatch.dispose();
+
+        mapManager.dispose();
     }
 
     private void logout() {
-        if (client.getPlayer() != null) {
+
+        Player player = client.getPlayer();
+
+        if (player != null) {
+
             PlayerLogoutPacket packet = new PlayerLogoutPacket();
-            packet.uuid = client.getPlayer().uuid;
+
+            packet.uuid = player.uuid;
+
             client.send(packet);
         }
+
         client.disconnect();
     }
+
+    @Override public void pause() {}
+    @Override public void resume() {}
 }

+ 6 - 10
core/src/main/java/me/lethunderhawk/screen/MenuScreen.java

@@ -72,26 +72,22 @@ public class MenuScreen implements Screen {
                 game.setScreen(gameScreen);
             }
         });
-        TextButton singlePlayerButton = new TextButton("Single Player", skin);
-        /*
-        singlePlayerButton.addListener(new ClickListener() {
+        TextButton settingsButton = new TextButton("Settings", skin);
+
+        settingsButton.addListener(new ClickListener() {
             @Override
             public void clicked(InputEvent event, float x, float y) {
-                String ip = ipField.getText();
-                if (ip == null || ip.trim().isEmpty()) {
-                    ip = "localhost";
-                }
-                GameScreen gameScreen = new GameScreen(game, ip);
-                game.setScreen(gameScreen);
+                SettingsScreen screen = new SettingsScreen(game);
+                game.setScreen(screen);
             }
         });
-        */
 
         // Layout components
 
         table.add(label).padBottom(10).row();
         table.add(ipField).width(300).padBottom(20).row();
         table.add(startButton).width(200).padBottom(20).row();
+        table.add(settingsButton).width(200).padBottom(20).row();
 /*
         table.add(singlePlayerButton).width(200).row();
 */

+ 174 - 0
core/src/main/java/me/lethunderhawk/screen/SettingsScreen.java

@@ -0,0 +1,174 @@
+package me.lethunderhawk.screen;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.Input;
+import com.badlogic.gdx.InputAdapter;
+import com.badlogic.gdx.Screen;
+import com.badlogic.gdx.assets.AssetManager;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.scenes.scene2d.InputEvent;
+import com.badlogic.gdx.scenes.scene2d.Stage;
+import com.badlogic.gdx.scenes.scene2d.ui.*;
+import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
+import com.badlogic.gdx.utils.viewport.ScreenViewport;
+import me.lethunderhawk.Main;
+import me.lethunderhawk.settings.Keybind;
+import me.lethunderhawk.settings.KeybindSettings;
+
+public class SettingsScreen implements Screen {
+    private final Main game;
+    private Stage stage;
+    private Skin skin;
+    private final AssetManager assets;
+
+    public SettingsScreen(Main main) {
+        this.game = main;
+        this.assets = new AssetManager();
+    }
+
+    @Override
+    public void show() {
+        stage = new Stage(new ScreenViewport());
+
+        try {
+            assets.load("ui/uiskin.json", Skin.class);
+            assets.finishLoading();
+            skin = assets.get("ui/uiskin.json", Skin.class);
+        } catch (Exception e) {
+            Gdx.app.error("MenuScreen", "Failed to load skin: ui/uiskin.json", e);
+            skin = new Skin();
+        }
+
+        Table table = new Table();
+        table.setFillParent(true);
+        table.center();
+
+        // spacing
+        table.defaults().pad(10);
+
+        stage.addActor(table);
+
+        // Title
+        Label title = new Label("Keybind Settings", skin);
+        table.add(title).colspan(2).row();
+
+        // Create one row per keybind
+        KeybindSettings.getKeybindMap().forEach((keybind, keycode) -> {
+
+            // Action label
+            Label actionLabel = new Label(formatKeybindName(keybind), skin);
+
+            // Current key button
+            TextButton keyButton = new TextButton(
+                Input.Keys.toString(keycode),
+                skin
+            );
+
+            // Click listener
+            keyButton.addListener(new ClickListener() {
+
+                private boolean waitingForInput = false;
+
+                @Override
+                public void clicked(InputEvent event, float x, float y) {
+
+                    if (waitingForInput)
+                        return;
+
+                    waitingForInput = true;
+
+                    keyButton.setText("Press key...");
+
+                    // Temporary input listener
+                    InputAdapter adapter = new InputAdapter() {
+                        @Override
+                        public boolean keyDown(int newKeycode) {
+
+                            // Save new keybind
+                            KeybindSettings.setKeybind(keybind, newKeycode);
+
+                            // Update button text
+                            keyButton.setText(Input.Keys.toString(newKeycode));
+
+                            // Restore stage input
+                            Gdx.input.setInputProcessor(stage);
+
+                            waitingForInput = false;
+
+                            return true;
+                        }
+                    };
+
+                    Gdx.input.setInputProcessor(adapter);
+                }
+            });
+
+            // Add row
+            table.add(actionLabel).left().width(200);
+            table.add(keyButton).width(200).row();
+        });
+
+        table.row();
+
+        TextButton backButton = new TextButton("Back", skin);
+
+        backButton.addListener(new ClickListener() {
+            @Override
+            public void clicked(InputEvent event, float x, float y) {
+                game.setScreen(new MenuScreen(game));
+            }
+        });
+
+// Add some top padding so it sits below the keybind list
+        table.add(backButton)
+            .colspan(2)
+            .center()
+            .width(250)
+            .padTop(30)
+            .row();
+
+        Gdx.input.setInputProcessor(stage);
+    }
+
+    private String formatKeybindName(Keybind keybind) {
+        String name = keybind.name().toLowerCase();
+
+        return Character.toUpperCase(name.charAt(0))
+            + name.substring(1);
+    }
+
+    @Override
+    public void render(float delta) {
+        Gdx.gl.glClearColor(0.12f, 0.12f, 0.12f, 1f); // dark background
+        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+
+        stage.act(Math.min(delta, 1/30f));
+        stage.draw();
+    }
+
+    @Override
+    public void resize(int width, int height) {
+        if (stage != null) {
+            stage.getViewport().update(width, height, true);
+        }
+    }
+
+    @Override
+    public void pause() {}
+    @Override
+    public void resume() {}
+    @Override
+    public void hide() {
+        // clear input when hidden
+        if (Gdx.input.getInputProcessor() == stage) {
+            Gdx.input.setInputProcessor(null);
+        }
+    }
+
+    @Override
+    public void dispose() {
+        if (stage != null) stage.dispose();
+        if (skin != null) skin.dispose();
+        if (assets != null) assets.dispose();
+    }
+}

+ 9 - 0
core/src/main/java/me/lethunderhawk/settings/Keybind.java

@@ -0,0 +1,9 @@
+package me.lethunderhawk.settings;
+
+public enum Keybind {
+    UP,
+    DOWN,
+    LEFT,
+    RIGHT,
+    BACK,
+}

+ 31 - 0
core/src/main/java/me/lethunderhawk/settings/KeybindSettings.java

@@ -0,0 +1,31 @@
+package me.lethunderhawk.settings;
+
+import com.badlogic.gdx.Input;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class KeybindSettings {
+    private KeybindSettings(){};
+    private static final Map<Keybind, Integer> keymap = new HashMap<>();
+
+    public static Map<Keybind, Integer> getKeybindMap() {
+        return new HashMap<>(keymap);
+    }
+
+    public static int getKeybind(Keybind key) {
+        return keymap.getOrDefault(key, -1);
+    }
+    public static void initialiseKeybinds(){
+        keymap.clear();
+        keymap.put(Keybind.UP, Input.Keys.W);
+        keymap.put(Keybind.DOWN, Input.Keys.S);
+        keymap.put(Keybind.LEFT, Input.Keys.A);
+        keymap.put(Keybind.RIGHT, Input.Keys.D);
+        keymap.put(Keybind.BACK, Input.Keys.ESCAPE);
+    }
+
+    public static void setKeybind(Keybind key, int value) {
+        keymap.put(key, value);
+    }
+}

+ 10 - 0
server/src/main/java/me/lethunderhawk/server/GameServerPacketListener.java

@@ -2,6 +2,7 @@ package me.lethunderhawk.server;
 
 import com.esotericsoftware.kryonet.Connection;
 import com.esotericsoftware.kryonet.Server;
+import me.lethunderhawk.constant.IntProperties;
 import me.lethunderhawk.network.packet.impl.*;
 import me.lethunderhawk.world.entity.player.Player;
 import me.lethunderhawk.network.packet.PacketHandler;
@@ -49,6 +50,11 @@ public class GameServerPacketListener implements PacketListener {
 
     @PacketHandler
     public void onLogin(PlayerLoginPacket packet, Connection connection){
+        if(!validatePlayer(packet.player)){
+            connection.close();
+            return;
+        }
+
         players.putIfAbsent(connection.getID(), packet.player);
 
         server.sendToAllExceptUDP(connection.getID(), packet);
@@ -67,4 +73,8 @@ public class GameServerPacketListener implements PacketListener {
             server.sendToUDP(connection.getID(), loginPacket);
         }
     }
+
+    private boolean validatePlayer(Player player) {
+        return !(player.getSpeed() > IntProperties.SPEED.getValue());
+    }
 }

+ 16 - 0
shared/src/main/java/me/lethunderhawk/constant/IntProperties.java

@@ -0,0 +1,16 @@
+package me.lethunderhawk.constant;
+
+public enum IntProperties {
+    SPEED(300f);
+
+    // ----- Instantiator
+
+    private final float value;
+    IntProperties(float value){
+        this.value = value;
+    }
+
+    public float getValue() {
+        return value;
+    }
+}

+ 6 - 0
shared/src/main/java/me/lethunderhawk/world/entity/player/Player.java

@@ -11,10 +11,12 @@ public class Player implements Collider {
     public float x;
     public float y;
     private final Rectangle bounds;
+    private final float speed;
 
     public Player(){
         this.uuid = UUID.randomUUID();
         this.avatar = new PlayerAvatar();
+        this.speed = 300f;
         avatar.initializeGrid();
         bounds = new Rectangle(
             x,
@@ -32,4 +34,8 @@ public class Player implements Collider {
     public Rectangle getBounds() {
         return bounds;
     }
+
+    public float getSpeed() {
+        return this.speed;
+    }
 }