From d18942196b9dd65b4cba8d7329b24532adf9fb2a Mon Sep 17 00:00:00 2001 From: paulaboks Date: Thu, 12 Mar 2026 17:09:25 -0300 Subject: [PATCH] 3D game --- client/components/camera.ts | 39 ++- client/components/dimension.ts | 1 + client/components/player_controls.ts | 6 +- client/game.ts | 2 +- client/generation.ts | 12 +- client/input_manager.ts | 106 ++++++- client/player.ts | 26 +- client/renderer/core.ts | 426 ++++++++------------------ client/renderer/math.ts | 84 ----- client/renderer/mod.ts | 1 + client/renderer/models.ts | 76 +++++ client/systems/dimension_logic.ts | 4 +- client/systems/player_controls.ts | 88 ++++-- client/systems/render_system.ts | 78 ++--- client/systems/rendering/dimension.ts | 30 +- client/systems/rendering/sprites.ts | 7 +- common/components/position.ts | 4 +- common/components/velocity.ts | 4 +- common/systems/movement_system.ts | 3 +- deno.json | 1 + deno.lock | 5 + 21 files changed, 422 insertions(+), 581 deletions(-) delete mode 100644 client/renderer/math.ts create mode 100644 client/renderer/models.ts diff --git a/client/components/camera.ts b/client/components/camera.ts index 5c967c3..77d4078 100644 --- a/client/components/camera.ts +++ b/client/components/camera.ts @@ -1,20 +1,17 @@ import { Component } from "$/common/ecs/mod.ts"; export class Camera extends Component { - active = true; - x: number; - y: number; - zoom: number; - rotation = 0; - offset_x = 0; - offset_y = 0; + x = 0; + y = 0; + z = 3; - constructor(x = 0, y = 0, zoom = 1) { - super(); - this.x = x; - this.y = y; - this.zoom = zoom; - } + pitch = 0; + yaw = 0; + roll = 0; + + fov = Math.PI / 3; + near = 0.1; + far = 1000; } export function screen_to_world( @@ -24,10 +21,10 @@ export function screen_to_world( screen_width: number, screen_height: number, ) { - return { - x: (screen_x - screen_width / 2) / camera.zoom + camera.x, - y: (screen_y - screen_height / 2) / camera.zoom + camera.y, - }; + // return { + // x: (screen_x - screen_width / 2) / camera.zoom + camera.x, + // y: (screen_y - screen_height / 2) / camera.zoom + camera.y, + // }; } export function world_to_screen( @@ -37,8 +34,8 @@ export function world_to_screen( screen_width: number, screen_height: number, ) { - return { - x: (world_x - camera.x) / camera.zoom + screen_width / 2, - y: (world_y - camera.y) / camera.zoom + screen_height / 2, - }; + // return { + // x: (world_x - camera.x) / camera.zoom + screen_width / 2, + // y: (world_y - camera.y) / camera.zoom + screen_height / 2, + // }; } diff --git a/client/components/dimension.ts b/client/components/dimension.ts index a9d7cbb..14a505f 100644 --- a/client/components/dimension.ts +++ b/client/components/dimension.ts @@ -7,6 +7,7 @@ import { Texture } from "../renderer/mod.ts"; export interface Tile { x: number; y: number; + z: number; id: string; data?: T; tickable?: boolean; diff --git a/client/components/player_controls.ts b/client/components/player_controls.ts index 0b6e60e..eb3629b 100644 --- a/client/components/player_controls.ts +++ b/client/components/player_controls.ts @@ -2,11 +2,11 @@ import { Component } from "$/common/ecs/mod.ts"; import { KeyCode } from "$/client/input_manager.ts"; export class PlayerControls extends Component { - move_speed: number = 100; + move_speed: number = 5; // Keys - move_up: KeyCode = "KeyW"; - move_down: KeyCode = "KeyS"; + move_forward: KeyCode = "KeyW"; + move_backwards: KeyCode = "KeyS"; move_left: KeyCode = "KeyA"; move_right: KeyCode = "KeyD"; diff --git a/client/game.ts b/client/game.ts index 76b836c..0527296 100644 --- a/client/game.ts +++ b/client/game.ts @@ -16,7 +16,7 @@ export function start_game(world: ClientWorld) { world.dimension = new Dimension([]); dimension.add(world.dimension); world.add_entity(dimension); - generate(dimension.get(Dimension)!, 100, 100); + generate(dimension.get(Dimension)!, 20, 20); create_player(world); diff --git a/client/generation.ts b/client/generation.ts index db90d1b..c3bbc3f 100644 --- a/client/generation.ts +++ b/client/generation.ts @@ -8,13 +8,13 @@ export function generate(dimension: Dimension, width: number, height: number, se const scale = 0.05; - for (let y = 0; y < height; y++) { + for (let z = 0; z < height; z++) { for (let x = 0; x < width; x++) { // multi-octave terrain const nx = x * scale; - const ny = y * scale; + const nz = z * scale; - let h = height_noise(nx, ny) * 1 + height_noise(nx * 2, ny * 2) * 0.5 + height_noise(nx * 4, ny * 4) * 0.25; + let h = height_noise(nx, nz) * 1 + height_noise(nx * 2, nz * 2) * 0.5 + height_noise(nx * 4, nz * 4) * 0.25; h /= 1.75; // normalize @@ -28,10 +28,10 @@ export function generate(dimension: Dimension, width: number, height: number, se id = "bworld:grass"; } - dimension.tiles.push({ x, y, id }); + dimension.tiles.push({ x, z, id, y: 0 }); // tree placement - if (id === "bworld:grass") { + /*if (id === "bworld:grass") { const t = tree_noise(x * 0.12, y * 0.12); if (t > 0.65) { @@ -41,7 +41,7 @@ export function generate(dimension: Dimension, width: number, height: number, se id: "bworld:tree", }); } - } + }*/ } } } diff --git a/client/input_manager.ts b/client/input_manager.ts index adaf0cf..e80c8c7 100644 --- a/client/input_manager.ts +++ b/client/input_manager.ts @@ -1,3 +1,5 @@ +import { canvas } from "./renderer/core.ts"; + export type KeyCode = // letters | "KeyA" @@ -89,9 +91,16 @@ export class InputManager { static typed_characters = new Set(); + static pointer_lock_flag = false; + static mouse_grab_timer = 0; + static mouse_ungrab_timer = 0; + static mouse_ungrab_timeout = -1; + static pointer_lock_waiting = false; + static initialize(canvas: HTMLCanvasElement) { self.addEventListener("keydown", (e) => { e.preventDefault(); + e.stopPropagation(); if (!this.keys_down.has(e.code)) { this.keys_pressed.add(e.code); } @@ -103,12 +112,18 @@ export class InputManager { self.addEventListener("keyup", (e) => { e.preventDefault(); + e.stopPropagation(); this.keys_down.delete(e.code); this.keys_released.add(e.code); }); self.addEventListener("mousedown", (e) => { e.preventDefault(); + e.stopPropagation(); + if (!document.hasFocus() && document.pointerLockElement !== canvas) { + this.set_mouse_grabbed(true); + return; + } if (!this.mouse_buttons_down.has(e.button)) { this.mouse_buttons_pressed.add(e.button); } @@ -121,22 +136,44 @@ export class InputManager { this.mouse_buttons_released.add(e.button); }); - self.addEventListener("mousemove", (e) => { - e.preventDefault(); - const rect = canvas.getBoundingClientRect(); - const new_x = e.clientX - rect.left; - const new_y = e.clientY - rect.top; - - this.mouse_delta_x += new_x - this.mouse_x; - this.mouse_delta_y += new_y - this.mouse_y; - - this.mouse_x = new_x; - this.mouse_y = new_y; - }); - self.addEventListener("wheel", (e) => { this.wheel_delta += e.deltaY; }); + + document.addEventListener("mousemove", (e) => { + e.preventDefault(); + if (document.pointerLockElement) { + this.mouse_delta_x += e.movementX; + this.mouse_delta_y += e.movementY; + } else { + const rect = canvas.getBoundingClientRect(); + const new_x = e.clientX - rect.left; + const new_y = e.clientY - rect.top; + + this.mouse_delta_x += new_x - this.mouse_x; + this.mouse_delta_y += new_y - this.mouse_y; + + this.mouse_x = new_x; + this.mouse_y = new_y; + } + }); + + document.addEventListener("pointerlockchange", () => { + self.setTimeout(() => { + const grab = document.pointerLockElement === canvas; + if (!grab) { + if (this.pointer_lock_flag) { + this.mouse_ungrab_timer = performance.now(); + } + } + this.pointer_lock_flag = grab; + }, 60); + this.pointer_lock_waiting = false; + }); + + document.addEventListener("pointerlockerror", () => { + this.pointer_lock_waiting = false; + }); } static is_key_down(code: KeyCode): boolean { @@ -197,4 +234,47 @@ export class InputManager { this.typed_characters.clear(); this.mouse_buttons_consumed.clear(); } + + static async set_mouse_grabbed(grab: boolean) { + this.pointer_lock_flag = grab; + const t = performance.now(); + this.mouse_grab_timer = t; + if (grab) { + this.mouse_x = -1; + this.mouse_y = -1; + this.pointer_lock_waiting = true; + try { + await canvas.requestPointerLock(); + } catch (_) { /* */ } + if (this.mouse_ungrab_timeout !== -1) { + self.clearTimeout(this.mouse_ungrab_timeout); + } + this.mouse_ungrab_timeout = -1; + if (t - this.mouse_ungrab_timer < 3000) { + this.mouse_ungrab_timeout = self.setTimeout(async () => { + try { + await canvas.requestPointerLock(); + } catch (_) { /* */ } + }, 3100 - (t - this.mouse_ungrab_timer)); + } + } else { + if (this.mouse_ungrab_timeout !== -1) self.clearTimeout(this.mouse_ungrab_timeout); + this.mouse_ungrab_timeout = -1; + if (!this.pointer_lock_waiting) { + document.exitPointerLock(); + } + } + } + + static is_mouse_grabbed() { + return document.hasFocus() && document.pointerLockElement === canvas; + } + + static toggle_fullscreen() { + if (!document.fullscreenElement) { + canvas.requestFullscreen(); + } else { + document.exitFullscreen(); + } + } } diff --git a/client/player.ts b/client/player.ts index 36c1a5c..4a0cdd6 100644 --- a/client/player.ts +++ b/client/player.ts @@ -4,9 +4,7 @@ import { Component, Entity } from "$/common/ecs/mod.ts"; import { Camera } from "$/client/components/camera.ts"; import { ItemStack, PlayerInventory } from "./inventory.ts"; import { PlayerControls } from "$/client/components/player_controls.ts"; -import { AnimatedSprite, Sprite } from "$/client/components/sprite.ts"; import { ClientWorld } from "./client_world.ts"; -import { AssetManager } from "./assets.ts"; import { GuiScreen } from "./gui/gui_screen.ts"; export class PlayerComponent extends Component { @@ -23,26 +21,8 @@ export class PlayerComponent extends Component { export function create_player(world: ClientWorld) { const player = new Entity("player"); - player.add(new Position(1440, 700)); + player.add(new Position(0, 0)); player.add(new Velocity(0, 0)); - player.add( - new AnimatedSprite("bworld:player", 72, 72, { - "idle": { - source_x: [0, 24], - source_y: [0, 0], - source_width: 24, - source_height: 24, - duration: 60, - }, - "running": { - source_x: [0, 24, 48, 72, 96, 120, 144, 168], - source_y: [24, 24, 24, 24, 24, 24, 24, 24], - source_width: 24, - source_height: 24, - duration: 20, - }, - }, "idle"), - ); player.add(new PlayerControls()); player.add(new PlayerComponent()); @@ -58,14 +38,12 @@ export function create_player(world: ClientWorld) { // player_inventory.container.add_item(new ItemStack("bworld:pumpkin_seeds", 64)); // player_inventory.container.add_item(new ItemStack("bworld:carrot_seeds", 64)); player_inventory.container.add_item(new ItemStack("bworld:furnace", 1)); - player.add(new Camera(-100, -100)); + player.add(new Camera()); world.add_entity(player); const player_hand = new Entity("playerhand"); player_hand.add(new Position(0, 0)); - // empty sprite - player_hand.add(new Sprite(AssetManager.instance.get("bworld:textures"), 0, 0)); world.add_entity(player_hand); world.add_tag("player", [player, player_hand]); diff --git a/client/renderer/core.ts b/client/renderer/core.ts index 09e78c4..13ce0ff 100644 --- a/client/renderer/core.ts +++ b/client/renderer/core.ts @@ -1,33 +1,21 @@ import { Camera } from "../components/camera.ts"; -import { mat4_mul, mat4_perspective, mat4_view } from "./math.ts"; +import { mat4 } from "gl-matrix"; + +export let gl: WebGLRenderingContext; +export let canvas: HTMLCanvasElement; const MAX_SPRITES = 10000; const VERTS_PER_SPRITE = 6; const FLOATS_PER_VERT = 9; -export let gl: WebGLRenderingContext; -export let canvas: HTMLCanvasElement; let program: WebGLProgram; let buffer: WebGLBuffer; -// const default_camera = new Camera(); -// let camera: Camera = default_camera; -// -// let camera_pos_loc: WebGLUniformLocation | null; -// let camera_zoom_loc: WebGLUniformLocation | null; -// let camera_rot_loc: WebGLUniformLocation | null; -// let camera_offset_loc: WebGLUniformLocation | null; - -export const projection = new Float32Array(16); -export const view = new Float32Array(16); -export const mvp_matrix = new Float32Array(16); - const vertex_data = new Float32Array(MAX_SPRITES * VERTS_PER_SPRITE * FLOATS_PER_VERT); let vert_index = 0; let pos_loc: GLint; let uv_loc: GLint; -let resolution_loc: WebGLUniformLocation | null; let texture_loc: WebGLUniformLocation | null; let mvp_loc: WebGLUniformLocation | null; let col_diffuse_loc: WebGLUniformLocation | null; @@ -35,22 +23,14 @@ let col_diffuse_loc: WebGLUniformLocation | null; let current_texture: WebGLTexture | null = null; export let white_tex: WebGLTexture | null = null; -export const camera = { - x: 3, - y: 3, - z: 3, - - pitch: 0, - yaw: 0, - roll: 0, - - fov: Math.PI / 3, - near: 0.1, - far: 1000, -}; - let mode3d = false; -let active_camera = camera; + +let camera: Camera | undefined; + +const ortho = mat4.create(); +const proj = mat4.create(); +const view = mat4.create(); +const mvp = mat4.create(); const vertex_src = `#version 300 es precision mediump float; @@ -121,15 +101,13 @@ export function init_window(canvas_element: HTMLCanvasElement) { gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); - gl.disable(gl.CULL_FACE); - create_white_texture(); - - update_mvp(canvas.width, canvas.height); } export function begin_drawing() { update_2d_mvp(); + gl.depthFunc(gl.LEQUAL); + gl.clearDepth(1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); vert_index = 0; } @@ -138,49 +116,35 @@ export function end_drawing() { flush_batch(); } -export const ortho_matrix = new Float32Array(16); - -export function update_2d_mvp() { +function update_2d_mvp() { const w = canvas.width; const h = canvas.height; - const l = 0, r = w, t = 0, b = h, n = -1, f = 1; - ortho_matrix[0] = 2 / (r - l); - ortho_matrix[1] = 0; - ortho_matrix[2] = 0; - ortho_matrix[3] = 0; + const l = 0; + const r = w; + const t = 0; + const b = h; + const n = -1; + const f = 1; - ortho_matrix[4] = 0; - ortho_matrix[5] = 2 / (t - b); - ortho_matrix[6] = 0; - ortho_matrix[7] = 0; - - ortho_matrix[8] = 0; - ortho_matrix[9] = 0; - ortho_matrix[10] = -2 / (f - n); - ortho_matrix[11] = 0; - - ortho_matrix[12] = -(r + l) / (r - l); - ortho_matrix[13] = -(t + b) / (t - b); - ortho_matrix[14] = -(f + n) / (f - n); - ortho_matrix[15] = 1; + mat4.ortho(ortho, l, r, b, t, n, f); } -export function begin_mode_2d(new_camera: Camera) { - mode3d = false; +//export function begin_mode_2d(new_camera: Camera) { +// mode3d = false; +// +// gl.disable(gl.DEPTH_TEST); +// +// vert_index = 0; +// +// update_2d_mvp(); +// gl.uniformMatrix4fv(mvp_loc, false, ortho_matrix); +//} - gl.disable(gl.DEPTH_TEST); - - vert_index = 0; - - update_2d_mvp(); - gl.uniformMatrix4fv(mvp_loc, false, ortho_matrix); -} - -export function end_mode_2d() { - flush_batch(); - // camera = default_camera; -} +// export function end_mode_2d() { +// flush_batch(); +// // camera = default_camera; +// } export function begin_clip(x: number, y: number, width: number, height: number) { gl.enable(gl.SCISSOR_TEST); @@ -194,6 +158,28 @@ export function end_clip() { gl.disable(gl.SCISSOR_TEST); } +export function begin_mode_3d(new_camera: Camera) { + flush_batch(); + + mode3d = true; + camera = new_camera; + + gl.enable(gl.DEPTH_TEST); + + update_camera(); +} + +export function end_mode_3d() { + if (!mode3d) { + return; + } + + flush_batch(); + + mode3d = false; + gl.disable(gl.DEPTH_TEST); +} + export function clear_background(r: number, g: number, b: number, a = 1) { gl.clearColor(r, g, b, a); gl.clear(gl.COLOR_BUFFER_BIT); @@ -208,9 +194,9 @@ export function flush_batch() { gl.bufferData(gl.ARRAY_BUFFER, vertex_data.subarray(0, vert_index), gl.STREAM_DRAW); if (mode3d) { - gl.uniformMatrix4fv(mvp_loc, false, mvp_matrix); + gl.uniformMatrix4fv(mvp_loc, false, mvp); } else { - gl.uniformMatrix4fv(mvp_loc, false, ortho_matrix); + gl.uniformMatrix4fv(mvp_loc, false, ortho); } gl.uniform4f(col_diffuse_loc, 1, 1, 1, 1); @@ -224,6 +210,44 @@ export function flush_batch() { vert_index = 0; } +export function resize_canvas() { + const width = self.innerWidth; + const height = self.innerHeight; + + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + canvas.style.width = canvas.width + "px"; + canvas.style.height = canvas.height + "px"; + + gl.viewport(0, 0, width, height); + + gl.useProgram(program); + } +} + +export function get_current_texture(): WebGLTexture | null { + return current_texture; +} + +export function set_current_texture(texture: WebGLTexture) { + current_texture = texture; +} + +export function push_vertex(px: number, py: number, pz: number, u: number, vv: number, r = 1, g = 1, b = 1, a = 1) { + let i = vert_index; + vertex_data[i++] = px; + vertex_data[i++] = py; + vertex_data[i++] = pz; + vertex_data[i++] = u; + vertex_data[i++] = vv; + vertex_data[i++] = r; + vertex_data[i++] = g; + vertex_data[i++] = b; + vertex_data[i++] = a; + vert_index = i; +} + export function push_quad( dx: number, dy: number, @@ -241,97 +265,41 @@ export function push_quad( const x2 = dx + dw; const y2 = dy + dh; - let i = vert_index; - // triangle 1 - vertex_data[i++] = dx; - vertex_data[i++] = dy; - vertex_data[i++] = 0; - vertex_data[i++] = u0; - vertex_data[i++] = v0; - vertex_data[i++] = r; - vertex_data[i++] = g; - vertex_data[i++] = b; - vertex_data[i++] = a; - - vertex_data[i++] = x2; - vertex_data[i++] = dy; - vertex_data[i++] = 0; - vertex_data[i++] = u1; - vertex_data[i++] = v0; - vertex_data[i++] = r; - vertex_data[i++] = g; - vertex_data[i++] = b; - vertex_data[i++] = a; - - vertex_data[i++] = dx; - vertex_data[i++] = y2; - vertex_data[i++] = 0; - vertex_data[i++] = u0; - vertex_data[i++] = v1; - vertex_data[i++] = r; - vertex_data[i++] = g; - vertex_data[i++] = b; - vertex_data[i++] = a; + push_vertex(dx, dy, 0, u0, v0, r, g, b, a); + push_vertex(x2, dy, 0, u1, v0, r, g, b, a); + push_vertex(dx, y2, 0, u0, v1, r, g, b, a); // triangle 2 - vertex_data[i++] = x2; - vertex_data[i++] = dy; - vertex_data[i++] = 0; - vertex_data[i++] = u1; - vertex_data[i++] = v0; - vertex_data[i++] = r; - vertex_data[i++] = g; - vertex_data[i++] = b; - vertex_data[i++] = a; - - vertex_data[i++] = x2; - vertex_data[i++] = y2; - vertex_data[i++] = 0; - vertex_data[i++] = u1; - vertex_data[i++] = v1; - vertex_data[i++] = r; - vertex_data[i++] = g; - vertex_data[i++] = b; - vertex_data[i++] = a; - - vertex_data[i++] = dx; - vertex_data[i++] = y2; - vertex_data[i++] = 0; - vertex_data[i++] = u0; - vertex_data[i++] = v1; - vertex_data[i++] = r; - vertex_data[i++] = g; - vertex_data[i++] = b; - vertex_data[i++] = a; - - vert_index = i; + push_vertex(x2, dy, 0, u1, v0, r, g, b, a); + push_vertex(x2, y2, 0, u1, v1, r, g, b, a); + push_vertex(dx, y2, 0, u0, v1, r, g, b, a); } -export function resize_canvas() { - const width = self.innerWidth; - const height = self.innerHeight; +// internal - if (canvas.width !== width || canvas.height !== height) { - canvas.width = width; - canvas.height = height; - canvas.style.width = canvas.width + "px"; - canvas.style.height = canvas.height + "px"; - - gl.viewport(0, 0, width, height); - - gl.useProgram(program); - // gl.uniform2f(resolution_loc, width, height); - update_mvp(canvas.width, canvas.height); +function update_camera() { + if (!camera) { + return; } -} -export function get_current_texture(): WebGLTexture | null { - return current_texture; -} + mat4.perspective( + proj, + camera.fov, + canvas.width / canvas.height, + camera.near, + camera.far, + ); -export function set_current_texture(texture: WebGLTexture) { - current_texture = texture; + mat4.identity(view); + + mat4.rotateZ(view, view, -camera.roll); + mat4.rotateX(view, view, -camera.pitch); + mat4.rotateY(view, view, -camera.yaw); + + mat4.translate(view, view, [-camera.x, -camera.y, -camera.z]); + + mat4.multiply(mvp, proj, view); } function compile_shader(type: number, src: string) { @@ -378,151 +346,3 @@ function create_white_texture() { white_tex = tex; } - -function update_mvp(width: number, height: number) { - const l = 0; - const r = width; - const b = height; - const t = 0; - const n = -1; - const f = 1; - - mvp_matrix[0] = 2 / (r - l); - mvp_matrix[1] = 0; - mvp_matrix[2] = 0; - mvp_matrix[3] = 0; - - mvp_matrix[4] = 0; - mvp_matrix[5] = 2 / (t - b); - mvp_matrix[6] = 0; - mvp_matrix[7] = 0; - - mvp_matrix[8] = 0; - mvp_matrix[9] = 0; - mvp_matrix[10] = -2 / (f - n); - mvp_matrix[11] = 0; - - mvp_matrix[12] = -(r + l) / (r - l); - mvp_matrix[13] = -(t + b) / (t - b); - mvp_matrix[14] = -(f + n) / (f - n); - mvp_matrix[15] = 1; -} - -export function push_cube( - x: number, - y: number, - z: number, - w: number, - h: number, - d: number, - r = 1, - g = 1, - b = 1, - a = 1, -) { - set_current_texture(white_tex!); - - const x2 = x + w; - const y2 = y + h; - const z2 = z + d; - - const u0 = 0, v0 = 0; - const u1 = 1, v1 = 1; - - let i = vert_index; - - function v(px: number, py: number, pz: number, u: number, vv: number) { - vertex_data[i++] = px; - vertex_data[i++] = py; - vertex_data[i++] = pz; - vertex_data[i++] = u; - vertex_data[i++] = vv; - vertex_data[i++] = r; - vertex_data[i++] = g; - vertex_data[i++] = b; - vertex_data[i++] = a; - } - - v(x, y, z2, u0, v0); - v(x2, y, z2, u1, v0); - v(x, y2, z2, u0, v1); - v(x2, y, z2, u1, v0); - v(x2, y2, z2, u1, v1); - v(x, y2, z2, u0, v1); - - v(x2, y, z, u0, v0); - v(x, y, z, u1, v0); - v(x2, y2, z, u0, v1); - v(x, y, z, u1, v0); - v(x, y2, z, u1, v1); - v(x2, y2, z, u0, v1); - - v(x, y, z, u0, v0); - v(x, y, z2, u1, v0); - v(x, y2, z, u0, v1); - v(x, y, z2, u1, v0); - v(x, y2, z2, u1, v1); - v(x, y2, z, u0, v1); - - v(x2, y, z2, u0, v0); - v(x2, y, z, u1, v0); - v(x2, y2, z2, u0, v1); - v(x2, y, z, u1, v0); - v(x2, y2, z, u1, v1); - v(x2, y2, z2, u0, v1); - - v(x, y2, z2, u0, v0); - v(x2, y2, z2, u1, v0); - v(x, y2, z, u0, v1); - v(x2, y2, z2, u1, v0); - v(x2, y2, z, u1, v1); - v(x, y2, z, u0, v1); - - v(x, y, z, u0, v0); - v(x2, y, z, u1, v0); - v(x, y, z2, u0, v1); - v(x2, y, z, u1, v0); - v(x2, y, z2, u1, v1); - v(x, y, z2, u0, v1); - - vert_index = i; -} - -export function begin_mode_3d() { - flush_batch(); - - mode3d = true; - active_camera = camera; - - gl.enable(gl.DEPTH_TEST); - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - vert_index = 0; - - update_camera(); -} - -export function end_mode_3d() { - if (!mode3d) { - return; - } - - flush_batch(); - - mode3d = false; - gl.disable(gl.DEPTH_TEST); -} - -export function update_camera() { - mat4_perspective( - projection, - camera.fov, - canvas.width / canvas.height, - camera.near, - camera.far, - ); - - mat4_view(view); - - mat4_mul(mvp_matrix, projection, view); -} diff --git a/client/renderer/math.ts b/client/renderer/math.ts deleted file mode 100644 index 9e05bc7..0000000 --- a/client/renderer/math.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { camera } from "./core.ts"; - -export function mat4_perspective(out: Float32Array, fov: number, aspect: number, near: number, far: number) { - const f = 1 / Math.tan(fov / 2); - - out[0] = f / aspect; - out[1] = 0; - out[2] = 0; - out[3] = 0; - - out[4] = 0; - out[5] = f; - out[6] = 0; - out[7] = 0; - - out[8] = 0; - out[9] = 0; - out[10] = -(far + near) / (far - near); - out[11] = -1; - - out[12] = 0; - out[13] = 0; - out[14] = -(2 * far * near) / (far - near); - out[15] = 0; -} - -export function mat4_view(out: Float32Array) { - const cx = camera.x; - const cy = camera.y; - const cz = camera.z; - - const yaw = camera.yaw; - const pitch = camera.pitch; - - const cosY = Math.cos(yaw); - const sinY = Math.sin(yaw); - const cosP = Math.cos(pitch); - const sinP = Math.sin(pitch); - - const fx = cosP * sinY; - const fy = sinP; - const fz = cosP * cosY; - - const rx = cosY; - const rz = -sinY; - - const ux = -sinP * sinY; - const uy = cosP; - const uz = -sinP * cosY; - - out[0] = rx; - out[1] = ux; - out[2] = -fx; - out[3] = 0; - - out[4] = 0; - out[5] = uy; - out[6] = -fy; - out[7] = 0; - - out[8] = rz; - out[9] = uz; - out[10] = -fz; - out[11] = 0; - - out[12] = -(rx * cx + rz * cz); - out[13] = -(ux * cx + uy * cy + uz * cz); - out[14] = fx * cx + fy * cy + fz * cz; - out[15] = 1; -} - -export function mat4_mul(out: Float32Array, a: Float32Array, b: Float32Array) { - for (let i = 0; i < 4; i++) { - const ai0 = a[i]; - const ai1 = a[i + 4]; - const ai2 = a[i + 8]; - const ai3 = a[i + 12]; - - out[i] = ai0 * b[0] + ai1 * b[1] + ai2 * b[2] + ai3 * b[3]; - out[i + 4] = ai0 * b[4] + ai1 * b[5] + ai2 * b[6] + ai3 * b[7]; - out[i + 8] = ai0 * b[8] + ai1 * b[9] + ai2 * b[10] + ai3 * b[11]; - out[i + 12] = ai0 * b[12] + ai1 * b[13] + ai2 * b[14] + ai3 * b[15]; - } -} diff --git a/client/renderer/mod.ts b/client/renderer/mod.ts index 62651b5..807e7da 100644 --- a/client/renderer/mod.ts +++ b/client/renderer/mod.ts @@ -2,4 +2,5 @@ export * from "./core.ts"; export * from "./shapes.ts"; export * from "./textures.ts"; export * from "./text.ts"; +export * from "./models.ts"; export * from "./types.ts"; diff --git a/client/renderer/models.ts b/client/renderer/models.ts new file mode 100644 index 0000000..b453d1c --- /dev/null +++ b/client/renderer/models.ts @@ -0,0 +1,76 @@ +import { flush_batch, get_current_texture, push_vertex, set_current_texture } from "./core.ts"; +import { Texture } from "./types.ts"; + +export function push_cube( + texture: Texture, + x: number, + y: number, + z: number, + w: number, + h: number, + d: number, + sx: number, + sy: number, + sw: number, + sh: number, + r = 1, + g = 1, + b = 1, + a = 1, +) { + if (get_current_texture() !== texture.tex) { + flush_batch(); + set_current_texture(texture.tex); + } + + const x2 = x + w; + const y2 = y + h; + const z2 = z + d; + + const u0 = sx / texture.width; + const v0 = sy / texture.height; + const u1 = (sx + sw) / texture.width; + const v1 = (sy + sh) / texture.height; + + push_vertex(x, y, z2, u0, v0, r, g, b, a); + push_vertex(x2, y, z2, u1, v0, r, g, b, a); + push_vertex(x, y2, z2, u0, v1, r, g, b, a); + push_vertex(x2, y, z2, u1, v0, r, g, b, a); + push_vertex(x2, y2, z2, u1, v1, r, g, b, a); + push_vertex(x, y2, z2, u0, v1, r, g, b, a); + + push_vertex(x2, y, z, u0, v0, r, g, b, a); + push_vertex(x, y, z, u1, v0, r, g, b, a); + push_vertex(x2, y2, z, u0, v1, r, g, b, a); + push_vertex(x, y, z, u1, v0, r, g, b, a); + push_vertex(x, y2, z, u1, v1, r, g, b, a); + push_vertex(x2, y2, z, u0, v1, r, g, b, a); + + push_vertex(x, y, z, u0, v0, r, g, b, a); + push_vertex(x, y, z2, u1, v0, r, g, b, a); + push_vertex(x, y2, z, u0, v1, r, g, b, a); + push_vertex(x, y, z2, u1, v0, r, g, b, a); + push_vertex(x, y2, z2, u1, v1, r, g, b, a); + push_vertex(x, y2, z, u0, v1, r, g, b, a); + + push_vertex(x2, y, z2, u0, v0, r, g, b, a); + push_vertex(x2, y, z, u1, v0, r, g, b, a); + push_vertex(x2, y2, z2, u0, v1, r, g, b, a); + push_vertex(x2, y, z, u1, v0, r, g, b, a); + push_vertex(x2, y2, z, u1, v1, r, g, b, a); + push_vertex(x2, y2, z2, u0, v1, r, g, b, a); + + push_vertex(x, y2, z2, u0, v0, r, g, b, a); + push_vertex(x2, y2, z2, u1, v0, r, g, b, a); + push_vertex(x, y2, z, u0, v1, r, g, b, a); + push_vertex(x2, y2, z2, u1, v0, r, g, b, a); + push_vertex(x2, y2, z, u1, v1, r, g, b, a); + push_vertex(x, y2, z, u0, v1, r, g, b, a); + + push_vertex(x, y, z, u0, v0, r, g, b, a); + push_vertex(x2, y, z, u1, v0, r, g, b, a); + push_vertex(x, y, z2, u0, v1, r, g, b, a); + push_vertex(x2, y, z, u1, v0, r, g, b, a); + push_vertex(x2, y, z2, u1, v1, r, g, b, a); + push_vertex(x, y, z2, u0, v1, r, g, b, a); +} diff --git a/client/systems/dimension_logic.ts b/client/systems/dimension_logic.ts index 1b0b839..916dc8c 100644 --- a/client/systems/dimension_logic.ts +++ b/client/systems/dimension_logic.ts @@ -11,7 +11,7 @@ import { distance_point_rectangle } from "../../common/utils.ts"; export class DimensionLogicSystem extends System { update(world: ClientWorld, delta: number): void { - const dimension_entity = world.get_entities().values().find((ent) => ent.get(Dimension)); + /*const dimension_entity = world.get_entities().values().find((ent) => ent.get(Dimension)); const dimension = dimension_entity!.get(Dimension)!; dimension.second_timer += delta; @@ -72,7 +72,7 @@ export class DimensionLogicSystem extends System { tile_info.on_click(world, tile); } } - } + }*/ } handle_second(world: ClientWorld, dimension: Dimension) { const tickables = dimension.tiles.filter((t) => t.tickable); diff --git a/client/systems/player_controls.ts b/client/systems/player_controls.ts index 414a56c..6a878d9 100644 --- a/client/systems/player_controls.ts +++ b/client/systems/player_controls.ts @@ -1,12 +1,10 @@ import { System } from "$/common/ecs/mod.ts"; import { Velocity } from "$/common/components/velocity.ts"; import { InputManager } from "../input_manager.ts"; -import { AnimatedSprite } from "../components/sprite.ts"; import { ClientWorld } from "../client_world.ts"; import { PlayerControls } from "../components/player_controls.ts"; import { Camera } from "../components/camera.ts"; import { Position } from "../../common/components/position.ts"; -import { canvas } from "../renderer/mod.ts"; import { PlayerComponent } from "../player.ts"; import { GuiPlayerInventory } from "../gui/gui_player_inventory.ts"; @@ -20,40 +18,51 @@ export class PlayerControlsSystem extends System { const velocity = player.get(Velocity)!; const controls = player.get(PlayerControls)!; const player_component = player.get(PlayerComponent)!; + const position = player.get(Position)!; + const camera = player.get(Camera)!; + + let input_x = 0; + let input_z = 0; if (InputManager.is_key_down(controls.move_left)) { - velocity.vx = -controls.move_speed; - } else if (InputManager.is_key_down(controls.move_right)) { - velocity.vx = controls.move_speed; - } else { - velocity.vx = 0; + input_x -= 1; + } + if (InputManager.is_key_down(controls.move_right)) { + input_x += 1; + } + if (InputManager.is_key_down(controls.move_forward)) { + input_z -= 1; + } + if (InputManager.is_key_down(controls.move_backwards)) { + input_z += 1; } - if (InputManager.is_key_down(controls.move_up)) { - velocity.vy = -controls.move_speed; - } else if (InputManager.is_key_down(controls.move_down)) { - velocity.vy = controls.move_speed; + const size = Math.hypot(input_x, input_z); + if (size > 0) { + input_x /= size; + input_z /= size; + } + + const sin = Math.sin(camera.yaw); + const cos = Math.cos(camera.yaw); + + const forwardX = sin; + const forwardZ = cos; + + const rightX = cos; + const rightZ = -sin; + + velocity.vx = (forwardX * input_z + rightX * input_x) * controls.move_speed; + velocity.vz = (forwardZ * input_z + rightZ * input_x) * controls.move_speed; + + if (InputManager.is_key_down("Space")) { + velocity.vy += controls.move_speed / 10; + } else if (InputManager.is_key_down("ShiftLeft")) { + velocity.vy -= controls.move_speed / 10; } else { velocity.vy = 0; } - const animated_sprite = player.get(AnimatedSprite)!; - - if ( - InputManager.is_key_down(controls.move_up) || InputManager.is_key_down(controls.move_down) || - InputManager.is_key_down(controls.move_left) || InputManager.is_key_down(controls.move_right) - ) { - animated_sprite.set_state("running"); - } else { - animated_sprite.set_state("idle"); - } - - if (InputManager.is_key_down(controls.move_left)) { - animated_sprite.flip_x = false; - } else if (InputManager.is_key_down(controls.move_right)) { - animated_sprite.flip_x = true; - } - if (InputManager.is_key_pressed(controls.open_inventory)) { if (player_component.screens.length === 0) { player_component.screens.push(new GuiPlayerInventory(player_component.player_inventory)); @@ -66,13 +75,24 @@ export class PlayerControlsSystem extends System { world.debugging = !world.debugging; } - const position = player.get(Position)!; - const camera = player.get(Camera)!; + if (InputManager.is_key_pressed("F11")) { + InputManager.toggle_fullscreen(); + } - camera.x = Math.round(position.x); - camera.y = Math.round(position.y); - camera.offset_x = Math.floor(canvas.width / 2); - camera.offset_y = Math.floor(canvas.height / 2); + camera.x = position.x; + camera.y = position.y + 1.79; + camera.z = position.z; + + if (InputManager.is_mouse_grabbed()) { + const mouse_delta = InputManager.get_mouse_delta(); + camera.yaw += -mouse_delta.x * 0.001; + camera.pitch += -mouse_delta.y * 0.001; + + const limit = Math.PI / 2 - 0.01; + camera.pitch = Math.max(-limit, Math.min(limit, camera.pitch)); + } + + InputManager.set_mouse_grabbed(player_component.screens.length === 0); if (player_component.screens.length === 0) { const player_inventory = player_component.player_inventory; diff --git a/client/systems/render_system.ts b/client/systems/render_system.ts index 015c2cc..6a08125 100644 --- a/client/systems/render_system.ts +++ b/client/systems/render_system.ts @@ -8,9 +8,8 @@ import { Camera } from "$/client/components/camera.ts"; import { render_animated_sprite, render_sprite } from "./rendering/sprites.ts"; import { render_dimension } from "./rendering/dimension.ts"; import { render_player_hotbar } from "./rendering/player.ts"; -import { begin_mode_2d, end_mode_2d } from "../renderer/mod.ts"; import { PlayerComponent } from "../player.ts"; -import { begin_mode_3d, end_mode_3d, push_cube } from "../renderer/core.ts"; +import { begin_mode_3d, end_mode_3d } from "../renderer/core.ts"; export class RenderSystem extends System { constructor() { @@ -18,83 +17,42 @@ export class RenderSystem extends System { } update(world: World, _delta: number): void { - /*const camera_entity = world.get_entities().values().find((e) => e.get(Camera)?.active); + const camera_entity = world.get_entities().values().find((e) => e.get(Camera)); const camera = camera_entity?.get(Camera); if (!camera) { return; } - begin_mode_2d(camera); + begin_mode_3d(camera); for (const entity of world.get_entities()) { - const position = entity.get(Position); - const sprite = entity.get(Sprite); - - if (sprite && position) { - render_sprite(sprite, position, camera); - } - - const animated_sprite = entity.get(AnimatedSprite); - - if (animated_sprite && position) { - render_animated_sprite(animated_sprite, position, camera); - } - const dimension = entity.get(Dimension); if (dimension) { - render_dimension(dimension, camera); + render_dimension(dimension /*, camera*/); } } - end_mode_2d(); + end_mode_3d(); for (const entity of world.get_entities()) { - const player_component = entity.get(PlayerComponent); + const position = entity.get(Position); + const sprite = entity.get(Sprite); + if (position && sprite) { + render_sprite(sprite, position); + } + + const animated_sprite = entity.get(AnimatedSprite); + if (position && animated_sprite) { + render_animated_sprite(animated_sprite, position); + } + + const player_component = entity.get(PlayerComponent); if (player_component) { render_player_hotbar(player_component.player_inventory); } - }*/ - - begin_mode_3d(); - push_cube( - -0.5, - -0.5, - -0.5, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - ); - push_cube( - 0.5, - 0.5, - 0.5, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - ); - push_cube( - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - ); - end_mode_3d(); + } } } diff --git a/client/systems/rendering/dimension.ts b/client/systems/rendering/dimension.ts index fb9da57..97530bb 100644 --- a/client/systems/rendering/dimension.ts +++ b/client/systems/rendering/dimension.ts @@ -1,23 +1,11 @@ import { get_sprite_region } from "$/common/utils.ts"; import { Dimension } from "$/client/components/dimension.ts"; -import { TEXTURE_SIZE, TILE_SIZE } from "$/common/constants.ts"; -import { Camera, world_to_screen } from "$/client/components/camera.ts"; -import { canvas, draw_texture_region } from "$/client/renderer/mod.ts"; +import { TEXTURE_SIZE } from "$/common/constants.ts"; import { EverythingRegistry, TileRegistry } from "$/common/everything_registry.ts"; +import { push_cube } from "$/client/renderer/mod.ts"; -export function render_dimension(dimension: Dimension, camera: Camera) { +export function render_dimension(dimension: Dimension /*, camera: Camera*/) { for (const tile of dimension.tiles) { - const x = tile.x * TILE_SIZE; - const y = tile.y * TILE_SIZE; - - const screen_position = world_to_screen(x, y, camera, canvas.width, canvas.height); - if ( - screen_position.x + TILE_SIZE < 0 || screen_position.x > canvas.width || - screen_position.y + TILE_SIZE < 0 || screen_position.y > canvas.height - ) { - continue; - } - let texture_id = tile.id; const tile_info = EverythingRegistry.get("tiles", tile.id); if (tile_info?.texture_id) { @@ -30,16 +18,18 @@ export function render_dimension(dimension: Dimension, camera: Camera) { const region = get_sprite_region(texture_id); - draw_texture_region( + push_cube( dimension.image, + tile.x, + tile.y, + tile.z, + 1, + 1, + 1, region.x * TEXTURE_SIZE, region.y * TEXTURE_SIZE, TEXTURE_SIZE, TEXTURE_SIZE, - x, - y, - TILE_SIZE, - TILE_SIZE, ); } } diff --git a/client/systems/rendering/sprites.ts b/client/systems/rendering/sprites.ts index e9b6a7a..aef3169 100644 --- a/client/systems/rendering/sprites.ts +++ b/client/systems/rendering/sprites.ts @@ -1,12 +1,8 @@ import { Position } from "$/common/components/position.ts"; import { AnimatedSprite, Sprite } from "$/client/components/sprite.ts"; import { draw_texture_region } from "$/client/renderer/mod.ts"; -import { Camera } from "$/client/components/camera.ts"; -// TODO: camera scale -// TODO: only render if inside screen - -export function render_sprite(sprite: Sprite, position: Position, _camera: Camera) { +export function render_sprite(sprite: Sprite, position: Position) { draw_texture_region( sprite.image, sprite.source_x, @@ -25,7 +21,6 @@ export function render_sprite(sprite: Sprite, position: Position, _camera: Camer export function render_animated_sprite( animated_sprite: AnimatedSprite, position: Position, - _camera: Camera, ) { const current_animation = animated_sprite.states[animated_sprite.current_state]; if (!current_animation) { diff --git a/common/components/position.ts b/common/components/position.ts index 55cbc49..b69f182 100644 --- a/common/components/position.ts +++ b/common/components/position.ts @@ -3,10 +3,12 @@ import { Component } from "$/common/ecs/component.ts"; export class Position extends Component { x: number; y: number; + z: number; - constructor(x: number, y: number) { + constructor(x: number, y: number, z = 0) { super(); this.x = x; this.y = y; + this.z = z; } } diff --git a/common/components/velocity.ts b/common/components/velocity.ts index 6e272f8..0dbe215 100644 --- a/common/components/velocity.ts +++ b/common/components/velocity.ts @@ -3,10 +3,12 @@ import { Component } from "$/common/ecs/component.ts"; export class Velocity extends Component { vx: number; vy: number; + vz: number; - constructor(vx: number, vy: number) { + constructor(vx: number, vy: number, vz = 0) { super(); this.vx = vx; this.vy = vy; + this.vz = vz; } } diff --git a/common/systems/movement_system.ts b/common/systems/movement_system.ts index 78a4cc2..d94fdcb 100644 --- a/common/systems/movement_system.ts +++ b/common/systems/movement_system.ts @@ -11,8 +11,7 @@ export class MovementSystem extends System { if (position && velocity) { position.x += velocity.vx * delta; position.y += velocity.vy * delta; - position.x = Math.round(position.x); - position.y = Math.round(position.y); + position.z += velocity.vz * delta; } } } diff --git a/deno.json b/deno.json index 5147f5e..897a831 100644 --- a/deno.json +++ b/deno.json @@ -18,6 +18,7 @@ "@gfx/canvas-wasm": "jsr:@gfx/canvas-wasm@^0.4.2", "@paulaboks/rng": "jsr:@paulaboks/rng@^0.0.2", "@std/fs": "jsr:@std/fs@^1.0.23", + "gl-matrix": "npm:gl-matrix@^3.4.4", "marked": "npm:marked@^17.0.3" } } diff --git a/deno.lock b/deno.lock index 85b5ff8..c85a0e8 100644 --- a/deno.lock +++ b/deno.lock @@ -7,6 +7,7 @@ "jsr:@std/fs@^1.0.23": "1.0.23", "jsr:@std/internal@^1.0.12": "1.0.12", "jsr:@std/path@^1.1.4": "1.1.4", + "npm:gl-matrix@^3.4.4": "3.4.4", "npm:marked@^17.0.3": "17.0.3" }, "jsr": { @@ -40,6 +41,9 @@ } }, "npm": { + "gl-matrix@3.4.4": { + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==" + }, "marked@17.0.3": { "integrity": "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==", "bin": true @@ -50,6 +54,7 @@ "jsr:@gfx/canvas-wasm@~0.4.2", "jsr:@paulaboks/rng@^0.0.2", "jsr:@std/fs@^1.0.23", + "npm:gl-matrix@^3.4.4", "npm:marked@^17.0.3" ] }