- Published on
1 Basic Game Framework and World Gneration
- Authors
- Name
- Yinhuan Yuan
Introduction
This is the first step to create a "TerraCraft 2D", a Minecraft-inspired sandbox game that demonstrates Phaser 3's core features through practical implementation. The game will include terrain generation, block manipulation, crafting, survival mechanics, and multiplayer capabilities.
- Project Structure
- Setup Instructions
- File Contents
- package.json
- tsconfig.json
- vite.config.ts
- index.html
- src/types/index.ts
- src/config/GameConfig.ts
- src/config/BlockTypes.ts
- src/world/NoiseGenerator.ts
- src/world/Chunk.ts
- src/world/WorldGenerator.ts
- src/world/ChunkManager.ts
- src/controllers/CameraController.ts
- src/scenes/PreloadScene.ts
- src/scenes/MenuScene.ts
- src/scenes/GameScene.ts
- src/main.ts
- .gitignore
- README.md
- Running the Development Server
- Building for Production
- Type Checking
- Controls
- Project Structure
- Architecture
- Future Improvements
- License
Project Structure
terracraft2d/
├── src/
│ ├── config/
│ │ ├── GameConfig.ts
│ │ └── BlockTypes.ts
│ ├── world/
│ │ ├── Chunk.ts
│ │ ├── ChunkManager.ts
│ │ ├── WorldGenerator.ts
│ │ └── NoiseGenerator.ts
│ ├── controllers/
│ │ └── CameraController.ts
│ ├── scenes/
│ │ ├── PreloadScene.ts
│ │ ├── MenuScene.ts
│ │ └── GameScene.ts
│ ├── types/
│ │ └── index.ts
│ └── main.ts
├── public/
│ └── assets/
│ ├── sprites/
│ ├── audio/
│ └── fonts/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md
Setup Instructions
# Create project directory
mkdir terracraft2d
cd terracraft2d
# Initialize package.json
npm init -y
# Install dependencies
npm install phaser
npm install -D vite typescript @types/node
# Create project structure
mkdir -p src/{config,world,controllers,scenes,types} public/assets/{sprites,audio,fonts}
File Contents
package.json
{
"name": "terracraft2d",
"version": "1.0.0",
"description": "2D Minecraft clone built with Phaser 3 and TypeScript",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"type-check": "tsc --noEmit"
},
"dependencies": {
"phaser": "^3.70.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0",
"vite": "^5.0.0"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true
},
"include": ["src"]
}
vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
base: './',
build: {
assetsInlineLimit: 0,
},
server: {
port: 3000,
host: true
}
});
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TerraCraft 2D - Vite + TypeScript</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #2c3e50;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: Arial, sans-serif;
}
#game-container {
box-shadow: 0 0 20px rgba(0,0,0,0.5);
border: 2px solid #34495e;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-size: 14px;
background: rgba(0,0,0,0.7);
padding: 10px;
border-radius: 5px;
user-select: none;
}
</style>
</head>
<body>
<div id="game-container"></div>
<div id="info">
<p>WASD/Arrow Keys: Move Camera</p>
<p>Q/E: Zoom In/Out</p>
<p>Mouse Wheel: Zoom</p>
<p>R: Reset Camera</p>
<p>G: Toggle Grid</p>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
src/types/index.ts
export interface BlockType {
id: number;
name: string;
color: number | null;
solid: boolean;
}
export interface ChunkData {
x: number;
y: number;
blocks: number[][];
}
export interface Vector2 {
x: number;
y: number;
}
src/config/GameConfig.ts
export const GameConfig = {
TILE_SIZE: 32,
CHUNK_SIZE: 16,
RENDER_DISTANCE: 3,
WORLD_HEIGHT: 128,
SEA_LEVEL: 64,
CAMERA_SPEED: 300,
ZOOM_SPEED: 0.1,
MIN_ZOOM: 0.5,
MAX_ZOOM: 3,
GAME_WIDTH: 1024,
GAME_HEIGHT: 768
} as const;
src/config/BlockTypes.ts
import { BlockType } from '../types';
export const BlockTypes: Record<string, BlockType> = {
AIR: { id: 0, name: 'Air', color: null, solid: false },
STONE: { id: 1, name: 'Stone', color: 0x808080, solid: true },
DIRT: { id: 2, name: 'Dirt', color: 0x8B4513, solid: true },
GRASS: { id: 3, name: 'Grass', color: 0x228B22, solid: true },
SAND: { id: 4, name: 'Sand', color: 0xF4E4B5, solid: true },
WATER: { id: 5, name: 'Water', color: 0x006994, solid: false },
WOOD: { id: 6, name: 'Wood', color: 0x8B4513, solid: true },
LEAVES: { id: 7, name: 'Leaves', color: 0x228B22, solid: true },
COAL: { id: 8, name: 'Coal', color: 0x2F2F2F, solid: true },
IRON: { id: 9, name: 'Iron', color: 0xAF8E77, solid: true },
BEDROCK: { id: 10, name: 'Bedrock', color: 0x1A1A1A, solid: true }
} as const;
export const getBlockById = (id: number): BlockType | undefined => {
return Object.values(BlockTypes).find(block => block.id === id);
};
src/world/NoiseGenerator.ts
export class NoiseGenerator {
private seed: number;
private p: number[];
constructor(seed: number = Date.now()) {
this.seed = seed;
this.p = this.createPermutation();
}
private createPermutation(): number[] {
const p: number[] = [];
for (let i = 0; i < 256; i++) {
p[i] = i;
}
let n = this.seed;
for (let i = 255; i > 0; i--) {
n = (n * 16807) % 2147483647;
const j = n % (i + 1);
[p[i], p[j]] = [p[j], p[i]];
}
return [...p, ...p];
}
private fade(t: number): number {
return t * t * t * (t * (t * 6 - 15) + 10);
}
private lerp(t: number, a: number, b: number): number {
return a + t * (b - a);
}
private grad(hash: number, x: number, y: number): number {
const h = hash & 3;
const u = h < 2 ? x : y;
const v = h < 2 ? y : x;
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
public noise(x: number, y: number): number {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
x -= Math.floor(x);
y -= Math.floor(y);
const u = this.fade(x);
const v = this.fade(y);
const A = this.p[X] + Y;
const B = this.p[X + 1] + Y;
return this.lerp(v,
this.lerp(u, this.grad(this.p[A], x, y),
this.grad(this.p[B], x - 1, y)),
this.lerp(u, this.grad(this.p[A + 1], x, y - 1),
this.grad(this.p[B + 1], x - 1, y - 1))
);
}
public octaveNoise(x: number, y: number, octaves: number = 4, persistence: number = 0.5): number {
let total = 0;
let frequency = 1;
let amplitude = 1;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
total += this.noise(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= 2;
}
return total / maxValue;
}
}
src/world/Chunk.ts
import * as Phaser from 'phaser';
import { GameConfig } from '../config/GameConfig';
import { BlockTypes, getBlockById } from '../config/BlockTypes';
export class Chunk {
private scene: Phaser.Scene;
public chunkX: number;
public chunkY: number;
private blocks: number[][];
private graphics: Phaser.GameObjects.Graphics;
private gridGraphics: Phaser.GameObjects.Graphics;
private gridVisible: boolean = false;
constructor(scene: Phaser.Scene, chunkX: number, chunkY: number) {
this.scene = scene;
this.chunkX = chunkX;
this.chunkY = chunkY;
this.blocks = [];
this.graphics = scene.add.graphics();
this.gridGraphics = scene.add.graphics();
const worldX = chunkX * GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE;
const worldY = chunkY * GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE;
this.graphics.setPosition(worldX, worldY);
this.gridGraphics.setPosition(worldX, worldY);
this.initialize();
}
private initialize(): void {
for (let x = 0; x < GameConfig.CHUNK_SIZE; x++) {
this.blocks[x] = [];
for (let y = 0; y < GameConfig.CHUNK_SIZE; y++) {
this.blocks[x][y] = BlockTypes.AIR.id;
}
}
}
public setBlock(localX: number, localY: number, blockId: number): void {
if (localX >= 0 && localX < GameConfig.CHUNK_SIZE &&
localY >= 0 && localY < GameConfig.CHUNK_SIZE) {
this.blocks[localX][localY] = blockId;
}
}
public getBlock(localX: number, localY: number): number | null {
if (localX >= 0 && localX < GameConfig.CHUNK_SIZE &&
localY >= 0 && localY < GameConfig.CHUNK_SIZE) {
return this.blocks[localX][localY];
}
return null;
}
public render(): void {
this.graphics.clear();
for (let x = 0; x < GameConfig.CHUNK_SIZE; x++) {
for (let y = 0; y < GameConfig.CHUNK_SIZE; y++) {
const blockId = this.blocks[x][y];
const blockType = getBlockById(blockId);
if (blockType && blockType.color !== null) {
this.graphics.fillStyle(blockType.color, 1);
this.graphics.fillRect(
x * GameConfig.TILE_SIZE,
y * GameConfig.TILE_SIZE,
GameConfig.TILE_SIZE,
GameConfig.TILE_SIZE
);
}
}
}
this.renderGrid();
}
private renderGrid(): void {
this.gridGraphics.clear();
if (!this.gridVisible) return;
this.gridGraphics.lineStyle(1, 0x333333, 0.3);
for (let x = 0; x <= GameConfig.CHUNK_SIZE; x++) {
this.gridGraphics.moveTo(x * GameConfig.TILE_SIZE, 0);
this.gridGraphics.lineTo(x * GameConfig.TILE_SIZE, GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE);
}
for (let y = 0; y <= GameConfig.CHUNK_SIZE; y++) {
this.gridGraphics.moveTo(0, y * GameConfig.TILE_SIZE);
this.gridGraphics.lineTo(GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE, y * GameConfig.TILE_SIZE);
}
this.gridGraphics.lineStyle(2, 0xFFFF00, 0.5);
this.gridGraphics.strokeRect(0, 0,
GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE,
GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE
);
}
public toggleGrid(visible: boolean): void {
this.gridVisible = visible;
this.renderGrid();
}
public destroy(): void {
this.graphics.destroy();
this.gridGraphics.destroy();
}
}
src/world/WorldGenerator.ts
import { GameConfig } from '../config/GameConfig';
import { BlockTypes } from '../config/BlockTypes';
import { NoiseGenerator } from './NoiseGenerator';
export class WorldGenerator {
private terrainNoise: NoiseGenerator;
private caveNoise: NoiseGenerator;
private oreNoise: NoiseGenerator;
constructor(seed: number = Date.now()) {
this.terrainNoise = new NoiseGenerator(seed);
this.caveNoise = new NoiseGenerator(seed + 1);
this.oreNoise = new NoiseGenerator(seed + 2);
}
public generateChunk(chunkX: number, chunkY: number): number[][] {
const chunk: number[][] = [];
for (let x = 0; x < GameConfig.CHUNK_SIZE; x++) {
chunk[x] = [];
for (let y = 0; y < GameConfig.CHUNK_SIZE; y++) {
const worldX = chunkX * GameConfig.CHUNK_SIZE + x;
const worldY = chunkY * GameConfig.CHUNK_SIZE + y;
chunk[x][y] = this.generateBlock(worldX, worldY);
}
}
return chunk;
}
private generateBlock(worldX: number, worldY: number): number {
const heightValue = this.terrainNoise.octaveNoise(worldX * 0.01, 0, 4, 0.6);
const height = Math.floor(GameConfig.SEA_LEVEL + heightValue * 30);
if (worldY < height - 5) {
return BlockTypes.AIR.id;
}
const caveValue = this.caveNoise.octaveNoise(worldX * 0.05, worldY * 0.05, 2, 0.5);
if (Math.abs(caveValue) < 0.1 && worldY > height) {
return BlockTypes.AIR.id;
}
if (worldY === height - 5) {
return BlockTypes.GRASS.id;
}
if (worldY > height - 5 && worldY < height) {
return BlockTypes.DIRT.id;
}
if (worldY >= GameConfig.WORLD_HEIGHT - 5) {
return BlockTypes.BEDROCK.id;
}
if (worldY >= height) {
const coalChance = this.oreNoise.noise(worldX * 0.1, worldY * 0.1);
if (coalChance > 0.8 && worldY < GameConfig.WORLD_HEIGHT - 20) {
return BlockTypes.COAL.id;
}
if (coalChance > 0.85 && worldY > GameConfig.WORLD_HEIGHT - 40) {
return BlockTypes.IRON.id;
}
return BlockTypes.STONE.id;
}
if (worldY >= GameConfig.SEA_LEVEL && worldY < height) {
return BlockTypes.WATER.id;
}
return BlockTypes.AIR.id;
}
}
src/world/ChunkManager.ts
import * as Phaser from 'phaser';
import { GameConfig } from '../config/GameConfig';
import { Chunk } from './Chunk';
import { WorldGenerator } from './WorldGenerator';
export class ChunkManager {
private scene: Phaser.Scene;
private worldGenerator: WorldGenerator;
private chunks: Map<string, Chunk>;
private gridVisible: boolean = false;
constructor(scene: Phaser.Scene, worldGenerator: WorldGenerator) {
this.scene = scene;
this.worldGenerator = worldGenerator;
this.chunks = new Map();
}
private getChunkKey(chunkX: number, chunkY: number): string {
return `${chunkX},${chunkY}`;
}
public loadChunk(chunkX: number, chunkY: number): Chunk {
const key = this.getChunkKey(chunkX, chunkY);
if (this.chunks.has(key)) {
return this.chunks.get(key)!;
}
const chunk = new Chunk(this.scene, chunkX, chunkY);
const generatedBlocks = this.worldGenerator.generateChunk(chunkX, chunkY);
for (let x = 0; x < GameConfig.CHUNK_SIZE; x++) {
for (let y = 0; y < GameConfig.CHUNK_SIZE; y++) {
chunk.setBlock(x, y, generatedBlocks[x][y]);
}
}
chunk.render();
chunk.toggleGrid(this.gridVisible);
this.chunks.set(key, chunk);
return chunk;
}
public unloadChunk(chunkX: number, chunkY: number): void {
const key = this.getChunkKey(chunkX, chunkY);
const chunk = this.chunks.get(key);
if (chunk) {
chunk.destroy();
this.chunks.delete(key);
}
}
public updateVisibleChunks(cameraX: number, cameraY: number): void {
const centerChunkX = Math.floor(cameraX / (GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE));
const centerChunkY = Math.floor(cameraY / (GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE));
for (let dx = -GameConfig.RENDER_DISTANCE; dx <= GameConfig.RENDER_DISTANCE; dx++) {
for (let dy = -GameConfig.RENDER_DISTANCE; dy <= GameConfig.RENDER_DISTANCE; dy++) {
this.loadChunk(centerChunkX + dx, centerChunkY + dy);
}
}
this.chunks.forEach((chunk, key) => {
const distance = Math.max(
Math.abs(chunk.chunkX - centerChunkX),
Math.abs(chunk.chunkY - centerChunkY)
);
if (distance > GameConfig.RENDER_DISTANCE + 1) {
this.unloadChunk(chunk.chunkX, chunk.chunkY);
}
});
}
public toggleGrid(): void {
this.gridVisible = !this.gridVisible;
this.chunks.forEach(chunk => {
chunk.toggleGrid(this.gridVisible);
});
}
public getBlockAt(worldX: number, worldY: number): number | null {
const chunkX = Math.floor(worldX / GameConfig.CHUNK_SIZE);
const chunkY = Math.floor(worldY / GameConfig.CHUNK_SIZE);
const localX = worldX % GameConfig.CHUNK_SIZE;
const localY = worldY % GameConfig.CHUNK_SIZE;
const key = this.getChunkKey(chunkX, chunkY);
const chunk = this.chunks.get(key);
if (chunk) {
return chunk.getBlock(localX, localY);
}
return null;
}
public getChunkCount(): number {
return this.chunks.size;
}
}
src/controllers/CameraController.ts
import * as Phaser from 'phaser';
import { GameConfig } from '../config/GameConfig';
import { Vector2 } from '../types';
export class CameraController {
private scene: Phaser.Scene;
private camera: Phaser.Cameras.Scene2D.Camera;
private velocity: Vector2;
private cursors: Phaser.Types.Input.Keyboard.CursorKeys;
private wasd: Record<string, Phaser.Input.Keyboard.Key>;
private zoomKeys: Record<string, Phaser.Input.Keyboard.Key>;
private resetKey: Phaser.Input.Keyboard.Key;
constructor(scene: Phaser.Scene, camera: Phaser.Cameras.Scene2D.Camera) {
this.scene = scene;
this.camera = camera;
this.velocity = { x: 0, y: 0 };
this.cursors = scene.input.keyboard!.createCursorKeys();
this.wasd = scene.input.keyboard!.addKeys('W,S,A,D') as Record<string, Phaser.Input.Keyboard.Key>;
this.zoomKeys = scene.input.keyboard!.addKeys('Q,E') as Record<string, Phaser.Input.Keyboard.Key>;
this.resetKey = scene.input.keyboard!.addKey('R');
this.setupMouseWheel();
}
private setupMouseWheel(): void {
this.scene.input.on('wheel', (_pointer: Phaser.Input.Pointer, _gameObjects: any[], _deltaX: number, deltaY: number) => {
const zoomChange = deltaY > 0 ? -GameConfig.ZOOM_SPEED : GameConfig.ZOOM_SPEED;
this.setZoom(this.camera.zoom + zoomChange);
});
}
public update(delta: number): void {
this.velocity.x = 0;
this.velocity.y = 0;
if (this.cursors.left.isDown || this.wasd.A.isDown) {
this.velocity.x = -GameConfig.CAMERA_SPEED;
}
if (this.cursors.right.isDown || this.wasd.D.isDown) {
this.velocity.x = GameConfig.CAMERA_SPEED;
}
if (this.cursors.up.isDown || this.wasd.W.isDown) {
this.velocity.y = -GameConfig.CAMERA_SPEED;
}
if (this.cursors.down.isDown || this.wasd.S.isDown) {
this.velocity.y = GameConfig.CAMERA_SPEED;
}
const moveDistance = (delta / 1000) / this.camera.zoom;
this.camera.scrollX += this.velocity.x * moveDistance;
this.camera.scrollY += this.velocity.y * moveDistance;
if (this.zoomKeys.Q.isDown) {
this.setZoom(this.camera.zoom + GameConfig.ZOOM_SPEED * (delta / 1000));
}
if (this.zoomKeys.E.isDown) {
this.setZoom(this.camera.zoom - GameConfig.ZOOM_SPEED * (delta / 1000));
}
if (Phaser.Input.Keyboard.JustDown(this.resetKey)) {
this.resetCamera();
}
}
private setZoom(newZoom: number): void {
this.camera.zoom = Phaser.Math.Clamp(newZoom, GameConfig.MIN_ZOOM, GameConfig.MAX_ZOOM);
}
private resetCamera(): void {
this.camera.scrollX = 0;
this.camera.scrollY = 0;
this.camera.zoom = 1;
}
}
src/scenes/PreloadScene.ts
import * as Phaser from 'phaser';
export class PreloadScene extends Phaser.Scene {
constructor() {
super({ key: 'PreloadScene' });
}
preload(): void {
const width = this.cameras.main.width;
const height = this.cameras.main.height;
const progressBar = this.add.graphics();
const progressBox = this.add.graphics();
progressBox.fillStyle(0x222222, 0.8);
progressBox.fillRect(width / 2 - 160, height / 2 - 25, 320, 50);
const loadingText = this.make.text({
x: width / 2,
y: height / 2 - 60,
text: 'Loading TerraCraft 2D...',
style: {
font: '20px monospace',
color: '#ffffff'
}
});
loadingText.setOrigin(0.5, 0.5);
this.load.on('progress', (value: number) => {
progressBar.clear();
progressBar.fillStyle(0xffffff, 1);
progressBar.fillRect(width / 2 - 150, height / 2 - 15, 300 * value, 30);
});
this.load.on('complete', () => {
progressBar.destroy();
progressBox.destroy();
loadingText.destroy();
});
}
create(): void {
this.scene.start('MenuScene');
}
}
src/scenes/MenuScene.ts
import * as Phaser from 'phaser';
export class MenuScene extends Phaser.Scene {
constructor() {
super({ key: 'MenuScene' });
}
create(): void {
const { width, height } = this.cameras.main;
this.add.rectangle(0, 0, width, height, 0x2c3e50).setOrigin(0);
const title = this.add.text(width / 2, height / 3, 'TerraCraft 2D', {
fontSize: '48px',
fontFamily: 'Arial',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 6
}).setOrigin(0.5);
this.add.text(width / 2, height / 3 + 60, 'A 2D Minecraft Clone in Phaser 3', {
fontSize: '18px',
fontFamily: 'Arial',
color: '#cccccc'
}).setOrigin(0.5);
const playButton = this.add.rectangle(width / 2, height / 2 + 50, 200, 50, 0x3498db)
.setInteractive({ useHandCursor: true });
const playText = this.add.text(width / 2, height / 2 + 50, 'Play', {
fontSize: '24px',
fontFamily: 'Arial',
color: '#ffffff'
}).setOrigin(0.5);
playButton.on('pointerover', () => {
playButton.setFillStyle(0x2980b9);
});
playButton.on('pointerout', () => {
playButton.setFillStyle(0x3498db);
});
playButton.on('pointerdown', () => {
this.scene.start('GameScene');
});
this.add.text(10, height - 20, 'Phase 1: Basic Framework (TypeScript)', {
fontSize: '12px',
fontFamily: 'Arial',
color: '#666666'
});
}
}
src/scenes/GameScene.ts
import * as Phaser from 'phaser';
import { GameConfig } from '../config/GameConfig';
import { WorldGenerator } from '../world/WorldGenerator';
import { ChunkManager } from '../world/ChunkManager';
import { CameraController } from '../controllers/CameraController';
export class GameScene extends Phaser.Scene {
private worldGenerator!: WorldGenerator;
private chunkManager!: ChunkManager;
private cameraController!: CameraController;
private debugText!: Phaser.GameObjects.Text;
constructor() {
super({ key: 'GameScene' });
}
create(): void {
this.physics.world.setBounds(
-GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE * 10,
-GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE * 10,
GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE * 20,
GameConfig.CHUNK_SIZE * GameConfig.TILE_SIZE * 20
);
const worldSeed = Date.now();
this.worldGenerator = new WorldGenerator(worldSeed);
this.chunkManager = new ChunkManager(this, this.worldGenerator);
this.cameraController = new CameraController(this, this.cameras.main);
this.cameras.main.setBackgroundColor(0x87CEEB);
this.input.keyboard!.on('keydown-G', () => {
this.chunkManager.toggleGrid();
});
this.chunkManager.updateVisibleChunks(0, 0);
this.createDebugInfo();
}
private createDebugInfo(): void {
this.debugText = this.add.text(10, 10, '', {
fontSize: '14px',
fontFamily: 'monospace',
color: '#ffffff',
backgroundColor: 'rgba(0,0,0,0.7)',
padding: { x: 10, y: 5 }
}).setScrollFactor(0).setDepth(1000);
}
update(time: number, delta: number): void {
this.cameraController.update(delta);
this.chunkManager.updateVisibleChunks(
this.cameras.main.scrollX + this.cameras.main.width / 2,
this.cameras.main.scrollY + this.cameras.main.height / 2
);
const worldX = Math.floor((this.cameras.main.scrollX + this.cameras.main.width / 2) / GameConfig.TILE_SIZE);
const worldY = Math.floor((this.cameras.main.scrollY + this.cameras.main.height / 2) / GameConfig.TILE_SIZE);
const chunkX = Math.floor(worldX / GameConfig.CHUNK_SIZE);
const chunkY = Math.floor(worldY / GameConfig.CHUNK_SIZE);
this.debugText.setText([
`World Pos: ${worldX}, ${worldY}`,
`Chunk: ${chunkX}, ${chunkY}`,
`Loaded Chunks: ${this.chunkManager.getChunkCount()}`,
`Zoom: ${this.cameras.main.zoom.toFixed(2)}x`,
`FPS: ${Math.round(this.game.loop.actualFps)}`
]);
}
}
src/main.ts
import * as Phaser from 'phaser';
import { PreloadScene } from './scenes/PreloadScene';
import { MenuScene } from './scenes/MenuScene';
import { GameScene } from './scenes/GameScene';
import { GameConfig } from './config/GameConfig';
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: GameConfig.GAME_WIDTH,
height: GameConfig.GAME_HEIGHT,
parent: 'game-container',
backgroundColor: '#2c3e50',
physics: {
default: 'arcade',
arcade: {
debug: false
}
},
scene: [PreloadScene, MenuScene, GameScene],
pixelArt: true,
antialias: false
};
new Phaser.Game(config);
.gitignore
node_modules/
dist/
.vite/
*.log
.DS_Store
.env
.env.local
README.md
# TerraCraft 2D - Phaser 3 Minecraft Clone
A 2D Minecraft-inspired game built with Phaser 3, TypeScript, and Vite.
## Features
- Procedural world generation with Perlin noise
- Chunk-based infinite world system
- Multiple block types with different properties
- Cave generation and ore distribution
- Smooth camera controls with zoom
- Debug overlay with performance metrics
## Development
### Prerequisites
- Node.js 16+
- npm or yarn
### Installation
```bash
# Clone the repository
git clone <your-repo-url>
cd terracraft2d
# Install dependencies
npm install
Running the Development Server
npm run dev
The game will be available at http://localhost:3000
Building for Production
npm run build
The built files will be in the dist/
directory.
Type Checking
npm run type-check
Controls
- WASD/Arrow Keys: Move camera
- Q/E: Zoom in/out
- Mouse Wheel: Zoom
- R: Reset camera position
- G: Toggle chunk grid overlay
Project Structure
src/
├── config/ # Game configuration and constants
├── world/ # World generation and chunk management
├── controllers/ # Input and camera controllers
├── scenes/ # Phaser scenes
├── types/ # TypeScript type definitions
└── main.ts # Entry point
Architecture
World Generation
The world is generated using Perlin noise with multiple octaves:
- Terrain height generation
- Cave carving
- Ore distribution
Chunk System
- Chunks are 16x16 blocks
- Dynamic loading/unloading based on camera position
- Render distance of 3 chunks in each direction
Block System
Currently implemented blocks:
- Air, Stone, Dirt, Grass
- Sand, Water
- Wood, Leaves
- Coal Ore, Iron Ore
- Bedrock
Future Improvements
- Block breaking and placing
- Player character
- Inventory system
- Crafting system
- Day/night cycle
- Entities and mobs
- Multiplayer support
License
MIT
## Next Steps
1. **Install the project:**
```bash
# Create the project directory
mkdir terracraft2d
cd terracraft2d
# Copy all the files from the setup above
# Install dependencies
npm install
# Run the development server
npm run dev
Benefits of this TypeScript + Vite setup:
- Type Safety: Full TypeScript support with strict typing
- Hot Module Replacement: Instant updates during development
- Better IDE Support: IntelliSense, auto-completion, and refactoring
- Modern Build Tool: Fast builds with Vite
- Modular Architecture: Clean separation of concerns
- Easy Testing: Can add Jest or Vitest for unit tests
Key Improvements over the vanilla JS version:
- Proper module system with ES modules
- Type definitions for all game objects
- Better error catching at compile time
- Cleaner project organization
- Modern development workflow
The project is now ready for Phase 2 where you can add block interaction, player mechanics, and more advanced features!