Change how ticking works

This commit is contained in:
2026-09-25 16:13:38 -03:00
parent 213363d0be
commit 5fb23cf404
9 changed files with 169 additions and 92 deletions
+89 -48
View File
@@ -1,6 +1,13 @@
import { TICK_DELTA } from "$/common/constants.ts";
import type { ClientLevel } from "../level/client_level.ts";
// anything that exists in the level and moves, like minecraft's Entity. position is the middle of its feet
// how far inside a block face still counts as touching it, so boxes resting exactly on a face don't snag
const EPSILON = 1e-7;
// the two axes that aren't the one being moved along
const OTHER_AXES = [[1, 2], [0, 2], [0, 1]] as const;
// anything that exists in the level and moves, like minecraft's Entity. position is the middle of its feet.
// it's simulated in fixed ticks (common/constants.ts), frames draw it between its last two positions
export abstract class Entity {
id: string;
level: ClientLevel;
@@ -8,6 +15,10 @@ export abstract class Entity {
x = 0;
y = 0;
z = 0;
// where it was at the start of the tick, what frames interpolate from
prev_x = 0;
prev_y = 0;
prev_z = 0;
// blocks per second
vx = 0;
vy = 0;
@@ -39,62 +50,92 @@ export abstract class Entity {
return this.colliding_y === 1;
}
// jumps there, without interpolating from where it was
set_position(x: number, y: number, z: number) {
this.x = x;
this.y = y;
this.z = z;
this.x = this.prev_x = x;
this.y = this.prev_y = y;
this.z = this.prev_z = z;
}
abstract tick(delta: number): void;
// falls and moves by its velocity, stopping on each axis it would run into a block on
move(delta: number) {
this.vy += this.gravity * delta;
this.colliding_x = this.vx !== 0 && this.#collides(this.x + this.vx * delta, this.y, this.z)
? -Math.sign(this.vx)
: 0;
if (this.colliding_x !== 0) {
this.vx = 0;
}
this.colliding_y = this.vy !== 0 && this.#collides(this.x, this.y + this.vy * delta, this.z)
? -Math.sign(this.vy)
: 0;
if (this.colliding_y !== 0) {
this.vy = 0;
}
this.colliding_z = this.vz !== 0 && this.#collides(this.x, this.y, this.z + this.vz * delta)
? -Math.sign(this.vz)
: 0;
if (this.colliding_z !== 0) {
this.vz = 0;
}
this.x += this.vx * delta;
this.y += this.vy * delta;
this.z += this.vz * delta;
save_previous_position() {
this.prev_x = this.x;
this.prev_y = this.y;
this.prev_z = this.z;
}
// whether the collision box at x/y/z overlaps any block, unloaded chunks included
#collides(x: number, y: number, z: number) {
const min_x = Math.floor(x - this.width / 2);
const max_x = Math.floor(x + this.width / 2);
const min_y = Math.floor(y);
const max_y = Math.floor(y + this.height);
const min_z = Math.floor(z - this.width / 2);
const max_z = Math.floor(z + this.width / 2);
// where to draw it, partial_tick is how far the frame is between the last tick and the next
render_position(partial_tick: number) {
return {
x: this.prev_x + (this.x - this.prev_x) * partial_tick,
y: this.prev_y + (this.y - this.prev_y) * partial_tick,
z: this.prev_z + (this.z - this.prev_z) * partial_tick,
};
}
for (let bx = min_x; bx <= max_x; bx++) {
for (let by = min_y; by <= max_y; by++) {
for (let bz = min_z; bz <= max_z; bz++) {
if (this.level.get_block(bx, by, bz)) {
return true;
// one step of the game, TICK_DELTA seconds
abstract tick(): void;
// falls and moves by its velocity for one tick. like minecraft it moves along y, then x, then z, each time only
// as far as it can before touching a block, and stops its velocity on the axes it hit something
move() {
// the average of this tick's start and end speed, so the arc is the same at any tick rate
const wanted_y = (this.vy + this.gravity * TICK_DELTA / 2) * TICK_DELTA;
this.vy += this.gravity * TICK_DELTA;
const wanted = [this.vx * TICK_DELTA, wanted_y, this.vz * TICK_DELTA];
const half = this.width / 2;
const min = [this.x - half, this.y, this.z - half];
const max = [this.x + half, this.y + this.height, this.z + half];
const moved = [0, 0, 0];
for (const axis of [1, 0, 2]) {
const distance = this.#clip(min, max, axis, wanted[axis]);
min[axis] += distance;
max[axis] += distance;
moved[axis] = distance;
}
const hit = (axis: number) => moved[axis] !== wanted[axis] ? -Math.sign(wanted[axis]) : 0;
this.colliding_x = hit(0);
this.colliding_y = hit(1);
this.colliding_z = hit(2);
if (this.colliding_x !== 0) this.vx = 0;
if (this.colliding_y !== 0) this.vy = 0;
if (this.colliding_z !== 0) this.vz = 0;
this.x += moved[0];
this.y += moved[1];
this.z += moved[2];
}
// how far the box can go along an axis before it runs into a block, unloaded chunks included.
// blocks it's already inside don't stop it, so it can get out of them
#clip(min: number[], max: number[], axis: number, distance: number) {
if (distance === 0) {
return 0;
}
const [a, b] = OTHER_AXES[axis];
const from = Math.floor(Math.min(min[axis], min[axis] + distance));
const to = Math.floor(Math.max(max[axis], max[axis] + distance));
const position = [0, 0, 0];
for (let i = from; i <= to; i++) {
for (let j = Math.floor(min[a] + EPSILON); j <= Math.floor(max[a] - EPSILON); j++) {
for (let k = Math.floor(min[b] + EPSILON); k <= Math.floor(max[b] - EPSILON); k++) {
position[axis] = i;
position[a] = j;
position[b] = k;
if (!this.level.get_block(position[0], position[1], position[2])) {
continue;
}
if (distance > 0 && i >= max[axis] - EPSILON) {
distance = Math.min(distance, i - max[axis]);
} else if (distance < 0 && i + 1 <= min[axis] + EPSILON) {
distance = Math.max(distance, i + 1 - min[axis]);
}
}
}
}
return false;
return distance;
}
}
+11 -11
View File
@@ -3,8 +3,8 @@ import { InputManager } from "../input_manager.ts";
import { ClientInventories } from "../inventory.ts";
import { Player } from "./player.ts";
// how often the position goes to the server
const SEND_POSITION_INTERVAL = 1 / 10;
// the position goes to the server every other tick
const SEND_POSITION_TICKS = 2;
// the player this client controls, like minecraft's LocalPlayer
export class LocalPlayer extends Player {
@@ -14,22 +14,22 @@ export class LocalPlayer extends Player {
move_speed = 4;
jump_force = 6.7;
#send_timer = 0;
#ticks_since_sent = 0;
constructor(client: Client, id: string, name: string) {
super(client.level, id, name);
this.client = client;
}
tick(delta: number) {
tick() {
if (!this.client.screen) {
this.#apply_input();
}
this.move(delta);
this.#send_position(delta);
this.move();
this.#send_position();
}
// turns with the mouse, like minecraft's MouseHandler.turnPlayer
// turns with the mouse every frame, not every tick, like minecraft's MouseHandler.turnPlayer
turn(mouse_dx: number, mouse_dy: number) {
this.yaw += -mouse_dx * 0.001;
this.pitch += -mouse_dy * 0.001;
@@ -75,12 +75,12 @@ export class LocalPlayer extends Player {
}
}
#send_position(delta: number) {
this.#send_timer += delta;
if (this.#send_timer < SEND_POSITION_INTERVAL) {
#send_position() {
this.#ticks_since_sent += 1;
if (this.#ticks_since_sent < SEND_POSITION_TICKS) {
return;
}
this.#send_timer = 0;
this.#ticks_since_sent = 0;
this.client.connection.send({
type: "move",
x: this.x,
+3 -2
View File
@@ -1,4 +1,5 @@
import type { PlayerInfo } from "$/common/protocol.ts";
import { TICK_DELTA } from "$/common/constants.ts";
import type { ClientLevel } from "../level/client_level.ts";
import { Player } from "./player.ts";
@@ -32,8 +33,8 @@ export class RemotePlayer extends Player {
this.pitch = pitch;
}
tick(delta: number) {
const t = Math.min(1, delta * SMOOTHING);
tick() {
const t = Math.min(1, TICK_DELTA * SMOOTHING);
this.x += (this.target_x - this.x) * t;
this.y += (this.target_y - this.y) * t;
this.z += (this.target_z - this.z) * t;