สร้าง MAP ในเกมส์ด้วย PERLIN NOISE

สร้าง MAP ในเกมส์ด้วย PERLIN NOISE

ให้ AI สร้างตัวอย่างเล่นๆอันนึง เอาไว้มาศึกษาอีกที

วิธีนี้เท่าที่รู้ คือ มันไม่ใช่การสร้าง map เตรียมไว้ก่อน แต่เป็นการสร้าง map ตอนที่เราทำงาน ดังนั้นมันไม่ต้อง save อะไรเยอะ map สามารถกว้างใหญ่ได้ไม่จำกัด เป็น infinity
แล้วการเลื่อนซ้ายขวาก็ไม่ได้ทำให้หน้าตาของ map เปลี่ยนไปจากเดิม แต่เป็นการ generate map เพิ่มเข้าไปเรื่อยๆ
แต่ตอนนี้ ถ้าปิดแล้วเปิดใหม่ map จะเปลี่ยนหน้าตาไป

ต่อไปคงต้องทำให้มัน save state ได้ เวลาที่เรา quit จาก application

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * คลาสหลักสำหรับเกมสร้างแผนที่
 * สร้าง JFrame และเพิ่ม GamePanel เข้าไป
 */
public class MapGame {

    public static void main(String[] args) {
        // สร้าง Frame หลักของโปรแกรม
        JFrame frame = new JFrame("Map Based Game - ใช้ปุ่มลูกศรเพื่อเคลื่อนที่");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 800);
        frame.setLocationRelativeTo(null); // แสดงหน้าต่างกลางจอ

        // สร้างและเพิ่ม GamePanel ที่เป็นหัวใจหลักของเกม
        GamePanel gamePanel = new GamePanel();
        frame.add(gamePanel);

        frame.setVisible(true);
    }

    /**
     * GamePanel ทำหน้าที่วาดแผนที่และจัดการการควบคุม
     */
    static class GamePanel extends JPanel {

        // --- ค่าคงที่สำหรับปรับแต่งเกม ---
        private static final int TILE_SIZE = 16; // ขนาดของแต่ละไทล์ (pixel)
        private static final int CHUNK_WIDTH = 32; // จำนวนไทล์ในแนวนอนของ 1 chunk
        private static final int CHUNK_HEIGHT = 32; // จำนวนไทล์ในแนวตั้งของ 1 chunk
        private static final int MOVE_SPEED = 10; // ความเร็วในการเคลื่อนที่ (pixel)
        private static final double NOISE_SCALE = 0.05; // สเกลของ Perlin noise (ค่ายิ่งน้อย แผนที่ยิ่งเรียบ)
        private static final double RESOURCE_NOISE_SCALE = 0.2; // สเกลของ noise สำหรับสร้างทรัพยากร
        private static final double RESOURCE_THRESHOLD = 0.7; // ค่า noise ที่จะเริ่มสร้างทรัพยากร (0.0 - 1.0)

        // --- ตัวแปรสถานะของเกม ---
        private int cameraX = 0; // ตำแหน่งกล้องแนวนอน (World Coordinate)
        private int cameraY = 0; // ตำแหน่งกล้องแนวตั้ง (World Coordinate)

        // Cache สำหรับเก็บ Chunk ที่สร้างแล้ว Key คือตำแหน่งของ Chunk, Value คือข้อมูลไทล์ของ Chunk นั้น
        private final Map<Point, int[][]> chunkCache = new HashMap<>();
        private final PerlinNoise noiseGenerator = new PerlinNoise();
        private final PerlinNoise resourceNoiseGenerator = new PerlinNoise(new Random().nextInt()); // ใช้ seed แยกสำหรับทรัพยากร

        public GamePanel() {
            this.setFocusable(true); // ทำให้ Panel นี้รับ input จาก keyboard ได้
            this.setBackground(Color.BLACK);

            // เพิ่ม KeyListener เพื่อรับการกดปุ่ม
            this.addKeyListener(new KeyAdapter() {
                @Override
                public void keyPressed(KeyEvent e) {
                    handleKeyPress(e.getKeyCode());
                }
            });
        }

        /**
         * จัดการการกดปุ่มเพื่อเคลื่อนที่
         * @param keyCode รหัสของปุ่มที่ถูกกด
         */
        private void handleKeyPress(int keyCode) {
            switch (keyCode) {
                case KeyEvent.VK_UP:
                    cameraY -= MOVE_SPEED;
                    break;
                case KeyEvent.VK_DOWN:
                    cameraY += MOVE_SPEED;
                    break;
                case KeyEvent.VK_LEFT:
                    cameraX -= MOVE_SPEED;
                    break;
                case KeyEvent.VK_RIGHT:
                    cameraX += MOVE_SPEED;
                    break;
            }
            repaint(); // สั่งให้วาดหน้าจอใหม่หลังจากเคลื่อนที่
        }

        /**
         * เมธอดหลักที่ใช้ในการวาด Component ทั้งหมด
         */
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            // คำนวณขอบเขตของ Chunk ที่ต้องวาดบนหน้าจอ
            int startChunkX = (int) Math.floor((double) cameraX / (CHUNK_WIDTH * TILE_SIZE));
            int startChunkY = (int) Math.floor((double) cameraY / (CHUNK_HEIGHT * TILE_SIZE));
            int endChunkX = (int) Math.floor((double) (cameraX + getWidth()) / (CHUNK_WIDTH * TILE_SIZE));
            int endChunkY = (int) Math.floor((double) (cameraY + getHeight()) / (CHUNK_HEIGHT * TILE_SIZE));
            
            // วนลูปเพื่อวาดทุก Chunk ที่มองเห็น
            for (int cy = startChunkY; cy <= endChunkY; cy++) {
                for (int cx = startChunkX; cx <= endChunkX; cx++) {
                    drawChunk(g, cx, cy);
                }
            }
             // วาดตำแหน่งปัจจุบันของกล้องเพื่อการดีบัก
            g.setColor(Color.WHITE);
            g.drawString(String.format("Position: (%d, %d)", cameraX, cameraY), 10, 20);
        }
        
        /**
         * วาด Chunk 1 อันที่ตำแหน่ง (chunkX, chunkY)
         * @param g Graphics object
         * @param chunkX ตำแหน่ง Chunk แนวนอน
         * @param chunkY ตำแหน่ง Chunk แนวตั้ง
         */
        private void drawChunk(Graphics g, int chunkX, int chunkY) {
            Point chunkPos = new Point(chunkX, chunkY);
            int[][] tiles;

            // ตรวจสอบว่า Chunk นี้อยู่ใน cache หรือไม่
            if (chunkCache.containsKey(chunkPos)) {
                tiles = chunkCache.get(chunkPos); // ถ้ามีอยู่แล้ว ก็ดึงจาก cache
            } else {
                tiles = generateChunk(chunkX, chunkY); // ถ้ายังไม่มี ก็สร้างใหม่
                chunkCache.put(chunkPos, tiles); // แล้วเก็บลง cache
            }

            // วนลูปเพื่อวาดทุกไทล์ใน Chunk
            for (int y = 0; y < CHUNK_HEIGHT; y++) {
                for (int x = 0; x < CHUNK_WIDTH; x++) {
                    int tileType = tiles[y][x];
                    Color tileColor = getTileColor(tileType);

                    // คำนวณตำแหน่งที่จะวาดบนหน้าจอ
                    int drawX = chunkX * CHUNK_WIDTH * TILE_SIZE + x * TILE_SIZE - cameraX;
                    int drawY = chunkY * CHUNK_HEIGHT * TILE_SIZE + y * TILE_SIZE - cameraY;

                    g.setColor(tileColor);
                    g.fillRect(drawX, drawY, TILE_SIZE, TILE_SIZE);
                }
            }
        }
        
        /**
         * สร้างข้อมูลไทล์สำหรับ Chunk ที่กำหนดโดยใช้ Perlin Noise
         * @param chunkX ตำแหน่ง Chunk แนวนอน
         * @param chunkY ตำแหน่ง Chunk แนวตั้ง
         * @return 2D array ของ tile types
         */
        private int[][] generateChunk(int chunkX, int chunkY) {
            int[][] tiles = new int[CHUNK_HEIGHT][CHUNK_WIDTH];
            for (int y = 0; y < CHUNK_HEIGHT; y++) {
                for (int x = 0; x < CHUNK_WIDTH; x++) {
                    // คำนวณตำแหน่งไทล์ในโลกจริง (World Coordinate)
                    int worldX = chunkX * CHUNK_WIDTH + x;
                    int worldY = chunkY * CHUNK_HEIGHT + y;

                    // สร้างค่า noise สำหรับภูมิประเทศ
                    double elevation = noiseGenerator.eval(worldX * NOISE_SCALE, worldY * NOISE_SCALE);
                    
                    // กำหนดประเภทไทล์ตามค่า noise
                    int tileType;
                    if (elevation < -0.4) tileType = 0; // Deep Water
                    else if (elevation < -0.2) tileType = 1; // Shallow Water
                    else if (elevation < -0.1) tileType = 2; // Sand
                    else if (elevation < 0.5) tileType = 3; // Grass
                    else if (elevation < 0.8) tileType = 4; // Rock
                    else tileType = 5; // Snow

                    // ถ้าเป็นพื้นหญ้า (Grass) ให้ลองสุ่มทรัพยากร
                    if (tileType == 3) {
                        double resourceValue = resourceNoiseGenerator.eval(worldX * RESOURCE_NOISE_SCALE, worldY * RESOURCE_NOISE_SCALE);
                        // resourceValue จะมีค่าระหว่าง -1 ถึง 1 เราจึงต้องปรับให้อยู่ในช่วง 0-1
                        if ((resourceValue + 1) / 2 > RESOURCE_THRESHOLD) {
                           tileType = 6; // Resource
                        }
                    }

                    tiles[y][x] = tileType;
                }
            }
            return tiles;
        }

        /**
         * คืนค่าสีสำหรับไทล์แต่ละประเภท
         * @param tileType รหัสของประเภทไทล์
         * @return Color object
         */
        private Color getTileColor(int tileType) {
            switch (tileType) {
                case 0: return new Color(0, 0, 128); // Deep Water
                case 1: return new Color(0, 100, 255); // Shallow Water
                case 2: return new Color(240, 230, 140); // Sand
                case 3: return new Color(34, 139, 34);  // Grass
                case 4: return new Color(139, 137, 137); // Rock
                case 5: return new Color(255, 250, 250); // Snow
                case 6: return Color.RED; // Resource
                default: return Color.BLACK;
            }
        }
    }

    /**
     * Perlin Noise Generator
     * นี่คือการ υλοποίηση ของ "Improved Perlin Noise" โดย Ken Perlin
     * เพื่อให้ไฟล์นี้ทำงานได้ด้วยตัวเองโดยไม่ต้องพึ่ง library ภายนอก
     */
     static class PerlinNoise {
        private final int[] p = new int[512];

        public PerlinNoise() {
            this(new Random().nextInt());
        }

        public PerlinNoise(int seed) {
            int[] permutation = new int[256];
            for (int i = 0; i < 256; i++) {
                permutation[i] = i;
            }
            Random rand = new Random(seed);
            for (int i = 255; i > 0; i--) {
                int index = rand.nextInt(i + 1);
                int temp = permutation[index];
                permutation[index] = permutation[i];
                permutation[i] = temp;
            }
            for (int i = 0; i < 256; i++) {
                p[i] = p[i + 256] = permutation[i];
            }
        }

        public double eval(double x, double y) {
            int X = (int) Math.floor(x) & 255;
            int Y = (int) Math.floor(y) & 255;
            x -= Math.floor(x);
            y -= Math.floor(y);
            double u = fade(x);
            double v = fade(y);
            int A = p[X] + Y;
            int B = p[X + 1] + Y;
            return lerp(v, lerp(u, grad(p[A], x, y), grad(p[B], x - 1, y)),
                    lerp(u, grad(p[A + 1], x, y - 1), grad(p[B + 1], x - 1, y - 1)));
        }

        private static double fade(double t) {
            return t * t * t * (t * (t * 6 - 15) + 10);
        }

        private static double lerp(double t, double a, double b) {
            return a + t * (b - a);
        }

        private static double grad(int hash, double x, double y) {
            int h = hash & 15;
            double u = h < 8 ? x : y;
            double v = h < 4 ? y : h == 12 || h == 14 ? x : 0;
            return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
        }
    }
}