/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com.umgutils.game;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
/**
*
* @author Thada
* คลาสหลักสำหรับเกมสร้างแผนที่
* สร้าง JFrame และเพิ่ม GamePanel เข้าไป
* คลาสหลักสำหรับเกมสร้างแผนที่
* สร้าง JFrame และเพิ่ม GamePanel เข้าไป
* คลาสหลักสำหรับเกมสร้างแผนที่
* สร้าง 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);
// เพิ่ม listener เพื่อดักจับ event ตอนปิดหน้าต่าง และทำการเซฟเกม
frame.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
gamePanel.saveGame();
}
});
frame.setVisible(true);
}
/**
* GamePanel ทำหน้าที่วาดแผนที่และจัดการการควบคุม
*/
static class GamePanel extends JPanel {
// --- เพิ่มค่าคงที่สำหรับไฟล์เซฟ ---
private static final String SAVE_FILE = "save.dat";
// --- ค่าคงที่สำหรับปรับแต่งเกม ---
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)
// --- ตัวแปรสำหรับ Seed ของโลก ---
private int worldSeed;
private int resourceSeed;
// --- ตัวแปรสำหรับจัดการการลากเมาส์ ---
private Point mouseDragStart;
private Point cameraStart;
// Cache สำหรับเก็บ Chunk ที่สร้างแล้ว
private final Map<Point, int[][]> chunkCache = new HashMap<>();
// ประกาศตัวแปรไว้ก่อน แต่ยังไม่สร้าง object
private PerlinNoise noiseGenerator;
private PerlinNoise resourceNoiseGenerator;
public GamePanel() {
this.setFocusable(true); // ทำให้ Panel นี้รับ input จาก keyboard ได้
this.setBackground(Color.BLACK);
// โหลดเกม (ซึ่งจะตั้งค่า seed และตำแหน่ง)
loadGame();
// *** สร้าง Noise Generators โดยใช้ Seed ที่โหลดมา ***
this.noiseGenerator = new PerlinNoise(worldSeed);
this.resourceNoiseGenerator = new PerlinNoise(resourceSeed);
// เพิ่ม KeyListener เพื่อรับการกดปุ่ม
this.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
handleKeyPress(e.getKeyCode());
}
});
// --- เพิ่ม Mouse Listener สำหรับการลากแผนที่ ---
MouseAdapter mouseAdapter = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// เมื่อกดเมาส์, บันทึกจุดเริ่มต้นของการลาก และตำแหน่งของกล้อง ณ ตอนนั้น
mouseDragStart = e.getPoint();
cameraStart = new Point(cameraX, cameraY);
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
}
@Override
public void mouseDragged(MouseEvent e) {
if (mouseDragStart != null) {
// คำนวณระยะทางที่เมาส์ลากไป
int dx = e.getX() - mouseDragStart.x;
int dy = e.getY() - mouseDragStart.y;
// อัปเดตตำแหน่งกล้องตามระยะที่ลาก (ลากขวากล้องไปซ้าย, ลากซ้ายกล้องไปขวา)
cameraX = cameraStart.x - dx;
cameraY = cameraStart.y - dy;
// วาดแผนที่ใหม่
repaint();
}
}
@Override
public void mouseReleased(MouseEvent e) {
// เมื่อปล่อยเมาส์, รีเซ็ตค่าการลาก
mouseDragStart = null;
cameraStart = null;
setCursor(Cursor.getDefaultCursor());
}
};
addMouseListener(mouseAdapter);
addMouseMotionListener(mouseAdapter);
}
/**
* จัดการการกดปุ่มเพื่อเคลื่อนที่
* @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(); // สั่งให้วาดหน้าจอใหม่หลังจากเคลื่อนที่
}
/**
* เมธอดสำหรับเซฟตำแหน่งและ Seed ปัจจุบันลงไฟล์
*/
public void saveGame() {
Properties properties = new Properties();
properties.setProperty("cameraX", String.valueOf(cameraX));
properties.setProperty("cameraY", String.valueOf(cameraY));
// --- เซฟค่า Seed ลงไปด้วย ---
properties.setProperty("worldSeed", String.valueOf(worldSeed));
properties.setProperty("resourceSeed", String.valueOf(resourceSeed));
// ใช้ try-with-resources เพื่อให้แน่ใจว่า stream ถูกปิดเสมอ
try (OutputStream output = new FileOutputStream(SAVE_FILE)) {
properties.store(output, "Map Game Save Data");
System.out.println("Game saved successfully!");
} catch (IOException io) {
System.err.println("Error saving game state: " + io.getMessage());
}
}
/**
* เมธอดสำหรับโหลดตำแหน่งและ Seed จากไฟล์
*/
private void loadGame() {
File saveFile = new File(SAVE_FILE);
// ตรวจสอบว่ามีไฟล์เซฟอยู่หรือไม่
if (!saveFile.exists()) {
System.out.println("No save file found. Creating a new world.");
// --- ถ้าไม่มีเซฟ ให้สุ่ม Seed ใหม่สำหรับโลกใบนี้ ---
Random rand = new Random();
this.worldSeed = rand.nextInt();
this.resourceSeed = rand.nextInt();
this.cameraX = 0;
this.cameraY = 0;
return; // จบการทำงาน
}
Properties properties = new Properties();
// ใช้ try-with-resources เพื่อให้แน่ใจว่า stream ถูกปิดเสมอ
try (InputStream input = new FileInputStream(SAVE_FILE)) {
properties.load(input);
// โหลดค่าและแปลงเป็น Integer
this.cameraX = Integer.parseInt(properties.getProperty("cameraX", "0"));
this.cameraY = Integer.parseInt(properties.getProperty("cameraY", "0"));
// --- โหลดค่า Seed กลับมา ---
this.worldSeed = Integer.parseInt(properties.getProperty("worldSeed"));
this.resourceSeed = Integer.parseInt(properties.getProperty("resourceSeed"));
System.out.println("Game loaded successfully! Starting at (" + cameraX + ", " + cameraY + ")");
} catch (IOException | NumberFormatException ex) {
// จัดการกรณีไฟล์เสียหายหรืออ่านค่าไม่ได้
System.err.println("Could not load save file: " + ex.getMessage() + ". Creating a new world.");
// --- ถ้าไฟล์เซฟเสีย ก็สร้างโลกใหม่ ---
Random rand = new Random();
this.worldSeed = rand.nextInt();
this.resourceSeed = rand.nextInt();
this.cameraX = 0;
this.cameraY = 0;
}
}
/**
* เมธอดหลักที่ใช้ในการวาด 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);
}
}
// วาดตำแหน่งปัจจุบันของกล้องและ Seed เพื่อการดีบัก
g.setColor(Color.WHITE);
g.drawString(String.format("Position: (%d, %d)", cameraX, cameraY), 10, 20);
g.drawString(String.format("World Seed: %d", worldSeed), 10, 40);
}
/**
* วาด 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(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);
}
}
}