Container Truck Fleet Simulator – LibGDX Design Doc

เกมจำลองการเดินทางของรถหัวลาก–หางพ่วงบรรทุกตู้คอนเทนเนอร์บนแผนที่จริงแบบ 2D ด้วย LibGDX (Skeleton Code + คำอธิบาย)

Overview

ภาพรวมโปรเจกต์

เอกสารนี้เป็น HTML single file ที่รวบรวมทั้ง สถาปัตยกรรม และ Java skeleton code สำหรับเกม Container Truck Fleet Simulator ด้วย LibGDX โดยเน้นสองมิติหลักคือ

ด้านล่างนี้เป็นโครงสร้าง class หลักของโปรเจกต์ และ skeleton code สำหรับแต่ละ class พร้อมคำอธิบายภาษาไทยแบบละเอียด เพื่อให้สามารถนำไปต่อยอดจริงใน LibGDX ได้

รายการคลาสหลัก

  • Entry & Core
    • DesktopLauncher
    • GwtLauncher
    • TruckSimGame
  • Screens
    • MainMenuScreen
    • WorldMapScreen
    • ResultScreen
  • World & Map
    • WorldMapManager
    • Location

ต่อเนื่อง

  • Fleet & Jobs
    • TruckUnit
    • FleetManager
    • JobOrder
  • Simulation & Metrics
    • TripStats
    • CostCalculator
    • ScoreSystem
  • UI & HUD
    • HudOverlay

หมายเหตุ: โค้ดด้านล่างเป็น skeleton ไม่ครบทุกรายละเอียด แต่จัดโครงให้พร้อมต่อยอดในโปรเจกต์ LibGDX จริง

Core & Entry

Entry & Core Classes

DesktopLauncher.java
Entry point – Desktop

คลาสนี้ใช้สำหรับรันเกมบน Desktop (ช่วยทดสอบ logic ก่อน export ไป HTML5) โครงสร้างมาตรฐานของ LibGDX

package com.trucksim.desktop;

import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import com.trucksim.TruckSimGame;

/**
 * จุดเริ่มต้นสำหรับรันเกมบน Desktop
 * ใช้ทดสอบเกม logic, rendering, input ก่อน export ไป HTML5
 */
public class DesktopLauncher {
    public static void main (String[] arg) {
        LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
        config.title = "Container Truck Fleet Simulator";
        config.width = 1280;
        config.height = 720;
        config.resizable = true;

        new LwjglApplication(new TruckSimGame(), config);
    }
}
GwtLauncher.java
Entry point – HTML5 (GWT)

สำหรับ deployment แบบ Web/HTML5 LibGDX จะใช้ GWT backend คลาสนี้เป็นตัวกำหนดการตั้งค่าและ bootstrap เกมบน Browser

package com.trucksim.client;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.backends.gwt.GwtApplication;
import com.badlogic.gdx.backends.gwt.GwtApplicationConfiguration;
import com.trucksim.TruckSimGame;

/**
 * Entry point สำหรับรันเกมบน HTML5 ผ่าน GWT
 */
public class GwtLauncher extends GwtApplication {

    @Override
    public GwtApplicationConfiguration getConfig () {
        // กำหนดขนาด canvas บน browser
        return new GwtApplicationConfiguration(1280, 720);
    }

    @Override
    public ApplicationListener createApplicationListener () {
        // ใช้ core game class เดียวกับ Desktop
        return new TruckSimGame();
    }
}
TruckSimGame.java
Core Game Class

คลาสหลักของเกม ทำหน้าที่จัดการ Screen ต่าง ๆ (เมนู, แผนที่, หน้าสรุป) และเป็นศูนย์กลางของ resource/shared systems

package com.trucksim;

import com.badlogic.gdx.Game;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.trucksim.screens.MainMenuScreen;

/**
 * Core Game class ของ LibGDX
 * จัดการ SpriteBatch, Font, และ Screen หลัก ๆ
 */
public class TruckSimGame extends Game {

    public SpriteBatch batch;
    public BitmapFont font;

    @Override
    public void create() {
        batch = new SpriteBatch();
        font = new BitmapFont();

        // เริ่มแสดงจาก MainMenuScreen
        this.setScreen(new MainMenuScreen(this));
    }

    @Override
    public void render() {
        super.render();
    }

    @Override
    public void dispose() {
        if (batch != null) batch.dispose();
        if (font != null) font.dispose();
        if (getScreen() != null) getScreen().dispose();
    }
}

» จากคลาสนี้ เราจะสลับไปมาระหว่าง Screen เช่น MainMenuScreen, WorldMapScreen, ResultScreen ได้อย่างเป็นระบบ

Screens

Screen ต่าง ๆ ของเกม

MainMenuScreen.java
เมนูหลัก

หน้าจอเมนูหลัก ใช้ Scene2D UI สร้างปุ่มเริ่มเกม, เลือก mission, ดูสถิติ เบื้องต้นแสดงปุ่มเริ่ม simulation

package com.trucksim.screens;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.trucksim.TruckSimGame;

import com.badlogic.gdx.scenes.scene2d.InputEvent;

/**
 * หน้าจอเมนูหลักของเกม
 */
public class MainMenuScreen implements Screen {

    private final TruckSimGame game;
    private Stage stage;
    private Skin skin;

    public MainMenuScreen(TruckSimGame game) {
        this.game = game;
    }

    @Override
    public void show() {
        stage = new Stage(new ScreenViewport());
        Gdx.input.setInputProcessor(stage);

        // หมายเหตุ: ในโปรเจกต์จริงควรมี skin ไฟล์ .json/.atlas
        skin = new Skin(Gdx.files.internal("uiskin.json"));

        Table table = new Table();
        table.setFillParent(true);
        stage.addActor(table);

        TextButton startButton = new TextButton("Start Simulation", skin);
        startButton.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                game.setScreen(new WorldMapScreen(game));
            }
        });

        table.add(startButton).width(240).height(60);
    }

    @Override
    public void render(float delta) {
        Gdx.gl.glClearColor(0, 0, 0.1f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        stage.act(delta);
        stage.draw();
    }

    @Override
    public void resize(int width, int height) {
        stage.getViewport().update(width, height, true);
    }

    @Override
    public void pause() {}

    @Override
    public void resume() {}

    @Override
    public void hide() {
        dispose();
    }

    @Override
    public void dispose() {
        if (stage != null) stage.dispose();
        if (skin != null) skin.dispose();
    }
}
WorldMapScreen.java
หน้าจอแผนที่ & Simulation หลัก

หน้าจอนี้คือหัวใจของเกม แสดงแผนที่ 2D, รถหลายคัน (fleet), HUD และคำนวณค่าเวลาจำลอง, ระยะทาง, ค่าน้ำมัน และคะแนน

package com.trucksim.screens;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.maps.tiled.TiledMap;
import com.badlogic.gdx.maps.tiled.TmxMapLoader;
import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer;
import com.badlogic.gdx.utils.viewport.FitViewport;
import com.badlogic.gdx.utils.viewport.Viewport;

import com.trucksim.TruckSimGame;
import com.trucksim.world.WorldMapManager;
import com.trucksim.fleet.FleetManager;
import com.trucksim.ui.HudOverlay;

/**
 * หน้าจอหลักที่ใช้ render แผนที่ + fleet trucks + HUD
 */
public class WorldMapScreen implements Screen {

    private final TruckSimGame game;
    private OrthographicCamera camera;
    private Viewport viewport;

    private TiledMap map;
    private OrthogonalTiledMapRenderer mapRenderer;

    private WorldMapManager worldMapManager;
    private FleetManager fleetManager;
    private HudOverlay hud;

    // เวลาใน simulation (สมมุติว่า 1 วินาทีจริง = 1 นาทีในเกม เป็นต้น)
    private float simulationTimeMinutes = 0f;
    private float timeScale = 1f; // เอาไว้ปรับเร่ง/ช้า simulation

    public WorldMapScreen(TruckSimGame game) {
        this.game = game;
    }

    @Override
    public void show() {
        camera = new OrthographicCamera();
        viewport = new FitViewport(40, 22.5f, camera); // world units

        // โหลดแผนที่ TMX (ต้องมีไฟล์จริงใน assets)
        map = new TmxMapLoader().load("maps/port_area.tmx");
        mapRenderer = new OrthogonalTiledMapRenderer(map, 1f);

        worldMapManager = new WorldMapManager(map);

        fleetManager = new FleetManager(worldMapManager);
        fleetManager.initializeSampleFleet(); // สร้างรถตัวอย่างหลายคัน + job

        hud = new HudOverlay(game.batch, fleetManager);
    }

    @Override
    public void render(float delta) {
        // อัปเดตเวลา simulation (สามารถยืด/หดเวลาได้ผ่าน timeScale)
        float simDeltaMinutes = delta * 60f * timeScale;
        simulationTimeMinutes += simDeltaMinutes;

        // อัปเดต logic ของ fleet (เคลื่อนที่, เปลี่ยน state, อัปเดตสถิติ)
        fleetManager.update(simDeltaMinutes);

        // Clear screen
        Gdx.gl.glClearColor(0.02f, 0.05f, 0.1f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        // กำหนดตำแหน่งกล้อง (ในเวอร์ชันง่าย ๆ fix ไว้กลางแผนที่)
        camera.update();
        mapRenderer.setView(camera);

        // Render แผนที่ก่อน
        mapRenderer.render();

        // Render fleet (เราจะวาด sprite รถใน worldMapManager หรือ fleetManager)
        game.batch.setProjectionMatrix(camera.combined);
        game.batch.begin();
        fleetManager.render(game.batch);
        game.batch.end();

        // Render HUD ทับด้านบน
        hud.update(simulationTimeMinutes);
        hud.render();
    }

    @Override
    public void resize(int width, int height) {
        viewport.update(width, height);
        hud.resize(width, height);
    }

    @Override
    public void pause() {}

    @Override
    public void resume() {}

    @Override
    public void hide() {
        dispose();
    }

    @Override
    public void dispose() {
        if (mapRenderer != null) mapRenderer.dispose();
        if (map != null) map.dispose();
        if (hud != null) hud.dispose();
    }
}
ResultScreen.java
หน้าสรุปผล Mission

ใช้แสดงสรุป performance หลังจากจบ mission เช่น รวมระยะทาง, เวลารวม, ค่าน้ำมันรวม, ต้นทุนรวม และคะแนน

package com.trucksim.screens;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.trucksim.TruckSimGame;
import com.trucksim.sim.ScoreSystem;

import com.badlogic.gdx.scenes.scene2d.InputEvent;

/**
 * หน้าจอสรุปผลหลังจบ mission/level
 */
public class ResultScreen implements Screen {

    private final TruckSimGame game;
    private final ScoreSystem.ScoreSummary summary;

    private Stage stage;
    private Skin skin;

    public ResultScreen(TruckSimGame game, ScoreSystem.ScoreSummary summary) {
        this.game = game;
        this.summary = summary;
    }

    @Override
    public void show() {
        stage = new Stage(new ScreenViewport());
        Gdx.input.setInputProcessor(stage);
        skin = new Skin(Gdx.files.internal("uiskin.json"));

        Table table = new Table();
        table.setFillParent(true);
        stage.addActor(table);

        Label title = new Label("Mission Result", skin);
        Label stats = new Label(
                "Total Distance: " + summary.totalDistanceKm + " km\n" +
                "Total Fuel: " + summary.totalFuelLiters + " L\n" +
                "Total Cost: " + summary.totalCost + " THB\n" +
                "Score: " + summary.finalScore,
                skin
        );

        TextButton backButton = new TextButton("Back to Menu", skin);
        backButton.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                game.setScreen(new MainMenuScreen(game));
            }
        });

        table.add(title).padBottom(20).row();
        table.add(stats).padBottom(20).row();
        table.add(backButton).width(220).height(50);
    }

    @Override
    public void render(float delta) {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        stage.act(delta);
        stage.draw();
    }

    @Override
    public void resize(int width, int height) {
        stage.getViewport().update(width, height, true);
    }

    @Override
    public void pause() {}

    @Override
    public void resume() {}

    @Override
    public void hide() {
        dispose();
    }

    @Override
    public void dispose() {
        if (stage != null) stage.dispose();
        if (skin != null) skin.dispose();
    }
}
World & Map

World & Map Management

Location.java
ข้อมูลจุดสำคัญบนแผนที่

คลาสสำหรับเก็บข้อมูลจุดสำคัญ (POI) บนแผนที่ เช่น ลานตู้, โรงงาน, ท่าเรือ, ลานจอด โดยเก็บชื่อ ประเภท และพิกัดใน world units

package com.trucksim.world;

/**
 * แทนตำแหน่งสำคัญบนแผนที่ เช่น Depot, Factory, Port, Yard
 */
public class Location {

    public enum Type {
        DEPOT,
        FACTORY,
        PORT,
        YARD
    }

    private final String id;
    private final String name;
    private final Type type;

    // พิกัด world (เช่น หน่วย tile หรือ meters-scaled)
    private final float x;
    private final float y;

    public Location(String id, String name, Type type, float x, float y) {
        this.id = id;
        this.name = name;
        this.type = type;
        this.x = x;
        this.y = y;
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public Type getType() { return type; }
    public float getX() { return x; }
    public float getY() { return y; }
}
WorldMapManager.java
ตัวจัดการแผนที่ & ตำแหน่ง

ใช้บริหารจัดการแผนที่ TMX และตำแหน่งต่าง ๆ รวมถึง utility function สำหรับแปลงตำแหน่ง, คำนวณระยะทาง เป็นต้น

package com.trucksim.world;

import com.badlogic.gdx.maps.tiled.TiledMap;

import java.util.ArrayList;
import java.util.List;

/**
 * จัดการข้อมูลเกี่ยวกับแผนที่ เช่น Location ต่าง ๆ และ helper method
 */
public class WorldMapManager {

    private final TiledMap map;
    private final List<Location> locations = new ArrayList<>();

    public WorldMapManager(TiledMap map) {
        this.map = map;
        loadDefaultLocations();
    }

    /**
     * ในโปรเจกต์จริง อาจโหลดจาก Object Layer ใน TMX
     * ใน skeleton นี้จะสร้างตัวอย่างด้วยมือ
     */
    private void loadDefaultLocations() {
        locations.add(new Location("depot1", "Depot A", Location.Type.DEPOT, 5f, 10f));
        locations.add(new Location("factory1", "Factory A", Location.Type.FACTORY, 18f, 12f));
        locations.add(new Location("port1", "Port A", Location.Type.PORT, 30f, 8f));
        locations.add(new Location("yard1", "Truck Yard", Location.Type.YARD, 3f, 4f));
    }

    public List<Location> getLocations() {
        return locations;
    }

    public Location findById(String id) {
        for (Location loc : locations) {
            if (loc.getId().equals(id)) return loc;
        }
        return null;
    }

    /**
     * คำนวณระยะทาง (Euclidean) ระหว่างจุดสองจุดบนแผนที่
     */
    public float distance(Location a, Location b) {
        float dx = a.getX() - b.getX();
        float dy = a.getY() - b.getY();
        return (float) Math.sqrt(dx * dx + dy * dy);
    }
}
Fleet & Jobs

Fleet, Truck Unit & Job Orders

JobOrder.java
งานขนส่ง 1 เที่ยว

งาน 1 เที่ยว: อาจประกอบด้วยเส้นทางย่อยหลายช่วง เช่น Yard → Depot → Factory → Port โดยกำหนด origin/destination เป็น Location

package com.trucksim.fleet;

import com.trucksim.world.Location;

/**
 * แทนงานขนส่ง 1 เที่ยวของรถ 1 คัน
 */
public class JobOrder {

    private final String id;
    private final Location origin;
    private final Location destination;

    // ตัวอย่าง: Job ประเภท "รับตู้เปล่าจาก Depot ไปโรงงาน" หรือ "โรงงานไปท่าเรือ"
    private final String description;

    public enum Status {
        PENDING,
        IN_PROGRESS,
        COMPLETED
    }

    private Status status = Status.PENDING;

    public JobOrder(String id, Location origin, Location destination, String description) {
        this.id = id;
        this.origin = origin;
        this.destination = destination;
        this.description = description;
    }

    public String getId() { return id; }
    public Location getOrigin() { return origin; }
    public Location getDestination() { return destination; }
    public String getDescription() { return description; }

    public Status getStatus() { return status; }
    public void setStatus(Status status) { this.status = status; }
}
TruckUnit.java
รถหัวลาก + หางพ่วง 1 ชุด

แทนรถหัวลาก–หางพ่วง 1 คันในเกม เก็บตำแหน่ง, ความเร็ว, state ของงาน และ JobOrder ปัจจุบันที่กำลังทำ

package com.trucksim.fleet;

import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.trucksim.world.Location;

/**
 * แทนรถหัวลาก+หางพ่วง 1 คันในเกม
 */
public class TruckUnit {

    public enum State {
        IDLE,
        MOVING_TO_ORIGIN,
        LOADING,
        MOVING_TO_DESTINATION,
        UNLOADING
    }

    private final String id;
    private float x;
    private float y;

    private float speed = 12f; // หน่วยต่อชั่วโมง (หรือ world unit ต่อชั่วโมง)

    private State state = State.IDLE;
    private JobOrder currentJob;

    // เป้าหมายตำแหน่งที่กำลังวิ่งไป
    private Location targetLocation;

    // เก็บสถิติระยะทางรวมของคันนี้
    private float totalDistanceKm = 0f;

    public TruckUnit(String id, float startX, float startY) {
        this.id = id;
        this.x = startX;
        this.y = startY;
    }

    public void assignJob(JobOrder job) {
        this.currentJob = job;
        if (job != null) {
            this.state = State.MOVING_TO_ORIGIN;
            this.targetLocation = job.getOrigin();
            job.setStatus(JobOrder.Status.IN_PROGRESS);
        }
    }

    /**
     * อัปเดตตำแหน่งรถตามเวลา (simDeltaMinutes)
     */
    public void update(float simDeltaMinutes) {
        if (currentJob == null || targetLocation == null) return;

        float hours = simDeltaMinutes / 60f;
        float distanceThisStep = speed * hours; // world units per hour (สมมุติ ~ km)

        float dx = targetLocation.getX() - x;
        float dy = targetLocation.getY() - y;
        float distanceToTarget = (float) Math.sqrt(dx * dx + dy * dy);

        if (distanceToTarget <= distanceThisStep) {
            // ถึงเป้าหมาย
            x = targetLocation.getX();
            y = targetLocation.getY();
            totalDistanceKm += distanceToTarget;

            onReachTarget();
        } else {
            // เคลื่อนที่เข้าใกล้เป้าหมาย
            float ratio = distanceThisStep / distanceToTarget;
            x += dx * ratio;
            y += dy * ratio;
            totalDistanceKm += distanceThisStep;
        }
    }

    private void onReachTarget() {
        switch (state) {
            case MOVING_TO_ORIGIN:
                state = State.LOADING;
                // ในเวอร์ชันจริงใส่เวลา loading ถ้าต้องการ
                // จากนั้นตั้งเป้าไปที่ destination
                targetLocation = currentJob.getDestination();
                state = State.MOVING_TO_DESTINATION;
                break;
            case MOVING_TO_DESTINATION:
                state = State.UNLOADING;
                // หลัง UNLOADING เสร็จถือว่างานจบ
                if (currentJob != null) {
                    currentJob.setStatus(JobOrder.Status.COMPLETED);
                }
                state = State.IDLE;
                currentJob = null;
                targetLocation = null;
                break;
            default:
                break;
        }
    }

    public void render(SpriteBatch batch) {
        // ใน skeleton นี้ยังไม่วาด sprite จริง
        // ในโปรเจกต์จริงควรใช้ Texture/TextureRegion วาดรูปหัวลาก+หางพ่วงที่ตำแหน่ง (x, y)
    }

    public String getId() { return id; }
    public float getX() { return x; }
    public float getY() { return y; }
    public float getTotalDistanceKm() { return totalDistanceKm; }
    public State getState() { return state; }
}
FleetManager.java
บริหาร Fleet รถหลายคัน

จัดการ list ของ TruckUnit, สร้างรถตัวอย่าง, สร้าง JobOrder และแจกจ่ายงานให้อัตโนมัติ รวมถึงรวบรวมสถิติของทั้ง fleet

package com.trucksim.fleet;

import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.trucksim.world.WorldMapManager;
import com.trucksim.world.Location;

import java.util.ArrayList;
import java.util.List;

/**
 * บริหารจัดการรถหลายคันใน fleet
 */
public class FleetManager {

    private final WorldMapManager worldMapManager;
    private final List<TruckUnit> trucks = new ArrayList<>();
    private final List<JobOrder> jobs = new ArrayList<>();

    public FleetManager(WorldMapManager worldMapManager) {
        this.worldMapManager = worldMapManager;
    }

    /**
     * สร้างรถ + งานตัวอย่างสำหรับทดลอง simulation
     */
    public void initializeSampleFleet() {
        Location yard = worldMapManager.findById("yard1");
        Location depot = worldMapManager.findById("depot1");
        Location factory = worldMapManager.findById("factory1");
        Location port = worldMapManager.findById("port1");

        // รถสามคันออกจาก yard
        trucks.add(new TruckUnit("TRUCK-01", yard.getX(), yard.getY()));
        trucks.add(new TruckUnit("TRUCK-02", yard.getX() + 1, yard.getY()));
        trucks.add(new TruckUnit("TRUCK-03", yard.getX() - 1, yard.getY()));

        // Job ตัวอย่าง: Depot -> Factory, Factory -> Port
        jobs.add(new JobOrder("JOB-1", depot, factory, "Empty container to factory"));
        jobs.add(new JobOrder("JOB-2", factory, port, "Loaded container to port"));
        jobs.add(new JobOrder("JOB-3", depot, factory, "Empty container to factory"));

        // แจกจ่ายงานแบบง่าย ๆ: assign ตามลำดับ
        assignJobsToIdleTrucks();
    }

    public void update(float simDeltaMinutes) {
        // อัปเดตรถทุกคัน
        for (TruckUnit truck : trucks) {
            truck.update(simDeltaMinutes);
        }

        // ตรวจดูว่ามี truck ว่างแล้วหรือยัง ถ้ามี assign งานใหม่
        assignJobsToIdleTrucks();
    }

    private void assignJobsToIdleTrucks() {
        for (TruckUnit truck : trucks) {
            if (truck.getState() == TruckUnit.State.IDLE) {
                JobOrder pending = findNextPendingJob();
                if (pending != null) {
                    truck.assignJob(pending);
                }
            }
        }
    }

    private JobOrder findNextPendingJob() {
        for (JobOrder job : jobs) {
            if (job.getStatus() == JobOrder.Status.PENDING) {
                return job;
            }
        }
        return null;
    }

    public void render(SpriteBatch batch) {
        for (TruckUnit truck : trucks) {
            truck.render(batch);
        }
    }

    public List<TruckUnit> getTrucks() {
        return trucks;
    }

    public List<JobOrder> getJobs() {
        return jobs;
    }
}
Simulation & Metrics

TripStats, CostCalculator & ScoreSystem

TripStats.java
สถิติงานขนส่ง

ใช้เก็บข้อมูลสถิติ เช่น เวลารวม, ระยะทาง รวมถึงคำนวณจาก fleet ใน ResultScreen หรือ ScoreSystem

package com.trucksim.sim;

/**
 * เก็บสถิติพื้นฐานของการขนส่ง (อาจต่อยอด scope เป็นรายคัน/ราย mission)
 */
public class TripStats {

    public float totalDistanceKm;
    public float totalFuelLiters;
    public float totalCost;

    public float totalSimMinutes;

    public TripStats() {}
}
CostCalculator.java
คำนวณค่าน้ำมัน & ต้นทุน

ใช้สมมุติสูตรง่าย ๆ: น้ำมัน (ลิตร) = ระยะทาง / (kmPerLiter) และ ต้นทุน = Liters × ราคา/ลิตร สามารถต่อยอดให้ละเอียดขึ้นได้

package com.trucksim.sim;

/**
 * ฟังก์ชันช่วยคำนวณค่าน้ำมันและต้นทุนเบื้องต้น
 */
public class CostCalculator {

    private final float kmPerLiter;
    private final float fuelPricePerLiter;

    public CostCalculator(float kmPerLiter, float fuelPricePerLiter) {
        this.kmPerLiter = kmPerLiter;
        this.fuelPricePerLiter = fuelPricePerLiter;
    }

    public float estimateFuelLiters(float distanceKm) {
        if (kmPerLiter <= 0) return 0f;
        return distanceKm / kmPerLiter;
    }

    public float estimateFuelCost(float distanceKm) {
        float liters = estimateFuelLiters(distanceKm);
        return liters * fuelPricePerLiter;
    }
}
ScoreSystem.java
ระบบคะแนน & สรุปผล

ระบบนี้รวบรวม TripStats ของทั้ง fleet แล้วคำนวณคะแนนตาม performance เช่น ระยะทางรวม, เวลา, ค่าน้ำมัน และให้ summary ใช้ใน ResultScreen

package com.trucksim.sim;

import com.trucksim.fleet.FleetManager;
import com.trucksim.fleet.TruckUnit;

/**
 * ระบบคำนวณคะแนนจาก performance ของทั้ง fleet
 */
public class ScoreSystem {

    public static class ScoreSummary {
        public float totalDistanceKm;
        public float totalFuelLiters;
        public float totalCost;
        public float totalSimMinutes;
        public int finalScore;
    }

    private final FleetManager fleetManager;
    private final CostCalculator costCalculator;

    public ScoreSystem(FleetManager fleetManager, CostCalculator costCalculator) {
        this.fleetManager = fleetManager;
        this.costCalculator = costCalculator;
    }

    /**
     * สร้าง summary จากข้อมูล fleet + simulation time
     */
    public ScoreSummary buildSummary(float totalSimMinutes) {
        ScoreSummary summary = new ScoreSummary();

        // รวมระยะทางจากทุกคัน
        float totalDistance = 0f;
        for (TruckUnit truck : fleetManager.getTrucks()) {
            totalDistance += truck.getTotalDistanceKm();
        }
        summary.totalDistanceKm = totalDistance;

        summary.totalFuelLiters = costCalculator.estimateFuelLiters(totalDistance);
        summary.totalCost = costCalculator.estimateFuelCost(totalDistance);
        summary.totalSimMinutes = totalSimMinutes;

        // ตัวอย่างสูตรคะแนนง่าย ๆ
        // คะแนนเริ่มจาก 1000 แล้วลบตามเวลาและต้นทุน
        int score = 1000;
        score -= (int)(summary.totalSimMinutes / 10); // ยิ่งใช้นานยิ่งโดนลบ
        score -= (int)(summary.totalCost / 100); // ต้นทุนสูงโดนหักคะแนน
        if (score < 0) score = 0;
        summary.finalScore = score;

        return summary;
    }
}
UI & HUD

HudOverlay – ส่วนแสดงผลบนหน้าจอ

HUD แสดงข้อมูลสำคัญ เช่น เวลาจำลอง, จำนวนรถที่กำลังวิ่ง, ระยะทางรวม และค่าอื่น ๆ เพื่อให้ผู้เล่นมองเห็น performance ของ fleet แบบเรียลไทม์

HudOverlay.java
Scene2D HUD
package com.trucksim.ui;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.trucksim.fleet.FleetManager;
import com.trucksim.fleet.TruckUnit;

/**
 * HUD แสดงข้อมูลรวมของ simulation
 */
public class HudOverlay {

    private final Stage stage;
    private final Skin skin;

    private final FleetManager fleetManager;

    private Label timeLabel;
    private Label trucksLabel;
    private Label distanceLabel;

    public HudOverlay(SpriteBatch batch, FleetManager fleetManager) {
        this.fleetManager = fleetManager;

        stage = new Stage(new ScreenViewport(), batch);
        skin = new Skin(Gdx.files.internal("uiskin.json"));

        Table root = new Table();
        root.setFillParent(true);
        root.top().left().pad(10);
        stage.addActor(root);

        timeLabel = new Label("Time: 0 min", skin);
        trucksLabel = new Label("Trucks: 0", skin);
        distanceLabel = new Label("Total Distance: 0 km", skin);

        root.add(timeLabel).left().row();
        root.add(trucksLabel).left().row();
        root.add(distanceLabel).left().row();
    }

    public void update(float simulationTimeMinutes) {
        timeLabel.setText(String.format("Time: %.1f min", simulationTimeMinutes));

        int truckCount = fleetManager.getTrucks().size();
        trucksLabel.setText("Trucks: " + truckCount);

        float totalDistance = 0f;
        for (TruckUnit truck : fleetManager.getTrucks()) {
            totalDistance += truck.getTotalDistanceKm();
        }
        distanceLabel.setText(String.format("Total Distance: %.1f km", totalDistance));
    }

    public void render() {
        stage.act(Gdx.graphics.getDeltaTime());
        stage.draw();
    }

    public void resize(int width, int height) {
        stage.getViewport().update(width, height, true);
    }

    public void dispose() {
        stage.dispose();
        skin.dispose();
    }
}

» HUD นี้เป็นตัวอย่างเบื้องต้น สามารถต่อยอดให้แสดงสถานะของ Job แต่ละคัน, mission objectives, ปุ่มควบคุมความเร็ว simulation เป็นต้น

Summary & Next Steps

สรุป & แนวทางต่อยอด

HTML single file นี้ออกแบบให้เป็นเหมือน "Design Document + Code Skeleton" สำหรับเกม Container Truck Fleet Simulator บน LibGDX โดยมี class แยกตามหน้าที่ชัดเจน: Core Game, Screens, World/Map, Fleet/Jobs, Simulation Metrics, และ HUD

เพื่อให้กลายเป็นโปรเจกต์จริง คุณสามารถ:

จากจุดนี้ คุณสามารถใช้ skeleton นี้เป็นฐานเพื่อสร้าง "เกมสอนโลจิสติกส์" ที่ทั้งสมจริงและเล่นสนุกสำหรับพนักงานและนักศึกษาได้อย่างมีประสิทธิภาพ