Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9b695af
Add climbable trait to blocks
ChibChi Mar 18, 2025
de60a67
Merge branch 'master' of https://github.com/ChibChi/Cubyz into featur…
ChibChi Apr 2, 2025
9321f92
Add player climbing detection
ChibChi Apr 2, 2025
71799d0
Add climb speed and player input handling
ChibChi Apr 2, 2025
6793846
Use base friction
ChibChi Apr 2, 2025
b678a54
fmt stuff
ChibChi Apr 2, 2025
3b5e783
Add crouching support and lower hitbox max
ChibChi Apr 2, 2025
305623f
Check x and y directions for collisions and rename block name
ChibChi Apr 2, 2025
a2d08df
Use intersections instead of collisions
ChibChi Apr 3, 2025
139a811
fmt stuff
ChibChi Apr 3, 2025
8a7eeba
Move climb detection to touchBlocks
ChibChi Apr 13, 2025
afe8c43
fmt stuff
ChibChi Apr 13, 2025
8aecff4
Fix skipping of gaps between climbable blocks
ChibChi Apr 14, 2025
1b75076
Add climbSpeed for blocks
ChibChi Apr 28, 2025
adb838b
Add helper for area calculation
ChibChi Apr 28, 2025
1e87589
Add climb speed calculation
ChibChi Apr 28, 2025
3804eaf
Use the entire bounding box for climb detection
ChibChi Apr 28, 2025
1358687
Make ivy climbable
ChibChi May 1, 2025
9db69bf
Add slip velocity, so the player can not grab a climbable when fallin…
ChibChi May 1, 2025
46aef4d
Set the climbable trait implicitly instead of explicitly
ChibChi May 2, 2025
36c7d71
Only divide by totalArea when it is bigger than the player hitbox
ChibChi May 2, 2025
7d49716
Separate climb movement from regular movement
ChibChi May 2, 2025
e349310
Add slip velocity and reduce grab distance
ChibChi May 2, 2025
a42a1fe
Add climb friction
ChibChi May 2, 2025
1175085
Merge remote-tracking branch 'upstream/master' into feature/climbable
ChibChi May 2, 2025
b48a2c9
Fix plane rotation models being climable on the ground
ChibChi May 2, 2025
5734de1
fmt stuff
ChibChi May 2, 2025
1b025bb
Remove climb amount and store whether the player is touching a climba…
ChibChi May 4, 2025
84f3fb6
Add crouching off onto climbable blocks
ChibChi May 4, 2025
e4e5632
Change movement input
ChibChi May 8, 2025
53daac5
Refactor climb detection
ChibChi May 8, 2025
e753467
Change climb friction and remove gravity when climbing
ChibChi May 8, 2025
aa9bbfc
Refactor edge crouching and crouching down onto climbables
ChibChi May 8, 2025
88cbf66
Fix climb detection
ChibChi May 23, 2025
2ea8a0b
fmt stuff
ChibChi May 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/cubyz/blocks/ivy.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
.texture = "ivy.png",
},
.lodReplacement = "cubyz:air",
.climbSpeed = 4,
}
12 changes: 12 additions & 0 deletions src/blocks.zig
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ var _blockResistance: [maxBlockCount]f32 = undefined;

var _solid: [maxBlockCount]bool = undefined;
var _selectable: [maxBlockCount]bool = undefined;
var _climbable: [maxBlockCount]bool = undefined;
var _climbSpeed: [maxBlockCount]f32 = undefined;
var _blockDrops: [maxBlockCount][]BlockDrop = undefined;
/// Meaning undegradable parts of trees or other structures can grow through this block.
var _degradable: [maxBlockCount]bool = undefined;
Expand Down Expand Up @@ -115,6 +117,8 @@ pub fn register(_: []const u8, id: []const u8, zon: ZonElement) u16 {
_absorption[size] = zon.get(u32, "absorbedLight", 0xffffff);
_degradable[size] = zon.get(bool, "degradable", false);
_selectable[size] = zon.get(bool, "selectable", true);
_climbSpeed[size] = zon.get(f32, "climbSpeed", 0);
_climbable[size] = _climbSpeed[size] != 0;
_solid[size] = zon.get(bool, "solid", true);
_gui[size] = allocator.dupe(u8, zon.get([]const u8, "gui", ""));
_transparent[size] = zon.get(bool, "transparent", false);
Expand Down Expand Up @@ -291,6 +295,14 @@ pub const Block = packed struct { // MARK: Block
return _selectable[self.typ];
}

pub inline fn climbable(self: Block) bool {
return _climbable[self.typ];
}

pub inline fn climbSpeed(self: Block) f32 {
return _climbSpeed[self.typ];
}

pub inline fn blockDrops(self: Block) []BlockDrop {
return _blockDrops[self.typ];
}
Expand Down
217 changes: 182 additions & 35 deletions src/game.zig
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,89 @@ pub const collision = struct {
return @floatCast(friction/totalArea);
}

fn calculateIntersectingArea(block: Block, blockPos: Vec3d, boundingBox: Box, area: vec.Area) f64 {
const blockBox: Box = .{
.min = blockPos + @as(Vec3d, @floatCast(block.mode().model(block).model().min)),
.max = blockPos + @as(Vec3d, @floatCast(block.mode().model(block).model().max)),
};

var min: Vec2d = undefined;
var max: Vec2d = undefined;
switch(area) {
.xy => {
max = std.math.clamp(vec.xy(blockBox.max), vec.xy(boundingBox.min), vec.xy(boundingBox.max));
min = std.math.clamp(vec.xy(blockBox.min), vec.xy(boundingBox.min), vec.xy(boundingBox.max));
},
.xz => {
max = std.math.clamp(vec.xz(blockBox.max), vec.xz(boundingBox.min), vec.xz(boundingBox.max));
min = std.math.clamp(vec.xz(blockBox.min), vec.xz(boundingBox.min), vec.xz(boundingBox.max));
},
.yz => {
max = std.math.clamp(vec.yz(blockBox.max), vec.yz(boundingBox.min), vec.yz(boundingBox.max));
min = std.math.clamp(vec.yz(blockBox.min), vec.yz(boundingBox.min), vec.yz(boundingBox.max));
},
}

return (max[0] - min[0])*(max[1] - min[1]);
}

pub fn calculateClimbSpeed(comptime side: main.utils.Side, pos: Vec3d, hitBox: Box, cameraRotation: f32, defaultSpeed: f32) f32 {
const forward: Vec3d = vec.rotateZ(Vec3d{0, 1, 0}, cameraRotation);
const forwardOffset = forward*Vec3d{0.01, 0.01, 0};

const boundingBox: Box = .{
.min = pos + hitBox.min + forwardOffset,
.max = pos + hitBox.max + forwardOffset,
};

const minX: i32 = @intFromFloat(@floor(boundingBox.min[0] + 0.0001));
const maxX: i32 = @intFromFloat(@floor(boundingBox.max[0] - 0.0001));
const minY: i32 = @intFromFloat(@floor(boundingBox.min[1] + 0.0001));
const maxY: i32 = @intFromFloat(@floor(boundingBox.max[1] - 0.0001));
const minZ: i32 = @intFromFloat(@floor(boundingBox.min[2]));
const maxZ: i32 = @intFromFloat(@floor(boundingBox.max[2]));

var climbSpeed: f32 = 0;
var totalArea: f32 = 0;

var x = minX;
while(x <= maxX) : (x += 1) {
var y = minY;
while(y <= maxY) : (y += 1) {
var z = minZ;
while(z <= maxZ) : (z += 1) {
const _block = if(side == .client) main.renderer.mesh_storage.getBlock(x, y, z) else main.server.world.?.getBlock(x, y, z);

if(_block == null or !_block.?.climbable()) continue;

const blockPos: Vec3d = .{@floatFromInt(x), @floatFromInt(y), @floatFromInt(z)};

const areaXZ = calculateIntersectingArea(_block.?, blockPos, boundingBox, .xz);
const areaYZ = calculateIntersectingArea(_block.?, blockPos, boundingBox, .yz);
const area: f32 = @floatCast(areaXZ + areaYZ);

totalArea += area;
climbSpeed += area*_block.?.climbSpeed();
}
}
}

if(totalArea == 0) {
return defaultSpeed;
}

// calculate the hitbox area of the x or y direction, depending on the camera rotation
const maxAngle: comptime_float = @sin(45.0);
const zLength: f64 = hitBox.max[2] - hitBox.min[2];
const directionLength: f64 = if(forward[0] >= -maxAngle and forward[0] >= maxAngle) hitBox.max[0] - hitBox.min[0] else hitBox.max[1] - hitBox.min[1];
const hitboxArea: f32 = @floatCast(directionLength*zLength);
if(totalArea > hitboxArea) {
return climbSpeed/totalArea;
}

return @max(climbSpeed, defaultSpeed);
}

pub fn collideOrStep(comptime side: main.utils.Side, comptime dir: Direction, amount: f64, pos: Vec3d, hitBox: Box, steppingHeight: f64) Vec3d {
const index = @intFromEnum(dir);

Expand Down Expand Up @@ -382,7 +465,7 @@ pub const collision = struct {
return false;
}

pub fn touchBlocks(entity: main.server.Entity, hitBox: Box, side: main.utils.Side) void {
pub fn touchBlocks(entity: *main.server.Entity, hitBox: Box, side: main.utils.Side, slipVel: comptime_float) void {
const boundingBox: Box = .{.min = entity.pos + hitBox.min, .max = entity.pos + hitBox.max};

const minX: i32 = @intFromFloat(@floor(boundingBox.min[0] - 0.01));
Expand All @@ -398,6 +481,7 @@ pub const collision = struct {
const extentX: Vec3d = extent + Vec3d{0.01, -0.01, -0.01};
const extentY: Vec3d = extent + Vec3d{-0.01, 0.01, -0.01};
const extentZ: Vec3d = extent + Vec3d{-0.01, -0.01, 0.01};
const extendSurrounding: Vec3d = extent + Vec3d{0.01, 0.01, 0.01};

var posX: i32 = minX;
while(posX <= maxX) : (posX += 1) {
Expand All @@ -407,13 +491,38 @@ pub const collision = struct {
while(posZ <= maxZ) : (posZ += 1) {
const block: ?Block =
if(side == .client) main.renderer.mesh_storage.getBlock(posX, posY, posZ) else main.server.world.?.getBlock(posX, posY, posZ);
if(block == null or block.?.touchFunction() == null)
if(block == null or !block.?.climbable() and block.?.touchFunction() == null)
continue;

const touchX: bool = isBlockIntersecting(block.?, posX, posY, posZ, center, extentX);
const touchY: bool = isBlockIntersecting(block.?, posX, posY, posZ, center, extentY);

if(block.?.climbable() and !entity.climbing and entity.vel[2] > slipVel) missingNormal: {
const blockPos: Vec3d = .{@floatFromInt(posX), @floatFromInt(posY), @floatFromInt(posZ)};
const model = block.?.mode().model(block.?).model();
const modelBoundingBoxMax: Vec3d = model.max + blockPos;
const blockCollisionLine = modelBoundingBoxMax[2] - boundingBox.min[2];

const isFullBlock = model.max[2] > 0.9;
const isSideBlock = touchX or touchY;
const isBottomBlock = blockCollisionLine < 0.15 and isBlockIntersecting(block.?, posX, posY, posZ, center, extendSurrounding);

entity.touchingClimbable = entity.touchingClimbable or isSideBlock or isBottomBlock;
entity.climbing = isSideBlock and (!isBottomBlock or isFullBlock);

const normal = if(model.internalQuads.len > 0) model.internalQuads[0].quadInfo().normal else break :missingNormal;
const area: vec.Area = if(normal[0] != 0) .yz else if(normal[1] != 0) .xz else .xy;

if(calculateIntersectingArea(block.?, blockPos, boundingBox, area) <= 0)
entity.climbing = false;
}

if(block.?.touchFunction() == null)
continue;

const touchZ: bool = isBlockIntersecting(block.?, posX, posY, posZ, center, extentZ);
if(touchX or touchY or touchZ)
block.?.touchFunction().?(block.?, entity, posX, posY, posZ, touchX and touchY and touchZ);
block.?.touchFunction().?(block.?, entity.*, posX, posY, posZ, touchX and touchY and touchZ);
}
}
}
Expand Down Expand Up @@ -469,6 +578,7 @@ pub const Player = struct { // MARK: Player
pub var selectionPosition2: ?Vec3i = null;

pub var currentFriction: f32 = 0;
pub var currentClimbSpeed: f32 = 0;

pub var onGround: bool = false;
pub var jumpCooldown: f64 = 0;
Expand Down Expand Up @@ -833,34 +943,42 @@ pub fn update(deltaTime: f64) void { // MARK: update()
const gravity = 30.0;
const terminalVelocity = 90.0;
const airFrictionCoefficient = gravity/terminalVelocity; // λ = a/v in equillibrium
const climbFrictionCoefficient = gravity/3.0;
var move: Vec3d = .{0, 0, 0};

if(main.renderer.mesh_storage.getBlock(@intFromFloat(@floor(Player.super.pos[0])), @intFromFloat(@floor(Player.super.pos[1])), @intFromFloat(@floor(Player.super.pos[2]))) != null) {
var acc = Vec3d{0, 0, 0};
if(!Player.isFlying.load(.monotonic)) {
acc[2] = -gravity;
acc[2] = if(Player.super.climbing) 0 else -gravity;
}

Player.currentClimbSpeed = collision.calculateClimbSpeed(.client, Player.super.pos, Player.outerBoundingBox, -camera.rotation[2], 3);
Player.currentFriction = if(Player.isFlying.load(.monotonic)) 20 else collision.calculateFriction(.client, Player.super.pos, Player.outerBoundingBox, 20);
var baseFrictionCoefficient: f32 = Player.currentFriction;
var directionalFrictionCoefficients: Vec3f = @splat(0);
const speedMultiplier: f32 = if(Player.hyperSpeed.load(.monotonic)) 4.0 else 1.0;

if(!Player.onGround and !Player.isFlying.load(.monotonic)) {
if(!Player.onGround and !Player.super.climbing and !Player.isFlying.load(.monotonic)) {
baseFrictionCoefficient = airFrictionCoefficient;
} else if(Player.super.climbing) {
baseFrictionCoefficient = climbFrictionCoefficient;
}

var jumping: bool = false;
Player.jumpCooldown -= deltaTime;
// At equillibrium we want to have dv/dt = a - λv = 0 → a = λ*v
const fricMul = speedMultiplier*baseFrictionCoefficient;

const up = Vec3d{0, 0, 1};
const forward = vec.rotateZ(Vec3d{0, 1, 0}, -camera.rotation[2]);
const right = Vec3d{-forward[1], forward[0], 0};
var movementDir: Vec3d = .{0, 0, 0};
var movementSpeed: f64 = 0;
var climbDir: Vec3d = .{0, 0, 0};
var climbSpeed: f64 = 0;

if(main.Window.grabbed) {
const walkingSpeed: f64 = if(Player.crouching) 2 else 4;
const walkingSpeed: f64 = if(Player.crouching or Player.super.climbing) 2 else 4;
const onGroundOrFlying = Player.onGround or Player.isFlying.load(.monotonic);
if(KeyBoard.key("forward").value > 0.0) {
if(KeyBoard.key("sprint").pressed and !Player.crouching) {
if(Player.isGhost.load(.monotonic)) {
Expand All @@ -873,22 +991,33 @@ pub fn update(deltaTime: f64) void { // MARK: update()
movementSpeed = @max(movementSpeed, 8)*KeyBoard.key("forward").value;
movementDir += forward*@as(Vec3d, @splat(8*KeyBoard.key("forward").value));
}
} else {
} else if(Player.super.climbing and !Player.crouching) {
climbSpeed = Player.currentClimbSpeed*KeyBoard.key("forward").value;
climbDir += up;
movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("forward").value;
movementDir += forward*@as(Vec3d, @splat(walkingSpeed*KeyBoard.key("forward").value));
} else if((Player.super.climbing and !Player.crouching) or onGroundOrFlying) {
movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("forward").value;
movementDir += forward*@as(Vec3d, @splat(walkingSpeed*KeyBoard.key("forward").value));
}
}
if(KeyBoard.key("backward").value > 0.0) {
movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("backward").value;
movementDir += forward*@as(Vec3d, @splat(-walkingSpeed*KeyBoard.key("backward").value));
if((Player.super.climbing and !Player.crouching) or onGroundOrFlying) {
movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("backward").value;
movementDir += forward*@as(Vec3d, @splat(-walkingSpeed*KeyBoard.key("backward").value));
}
}
if(KeyBoard.key("left").value > 0.0) {
movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("left").value;
movementDir += right*@as(Vec3d, @splat(walkingSpeed*KeyBoard.key("left").value));
if((Player.super.climbing and !Player.crouching) or onGroundOrFlying) {
movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("left").value;
movementDir += right*@as(Vec3d, @splat(walkingSpeed*KeyBoard.key("left").value));
}
}
if(KeyBoard.key("right").value > 0.0) {
movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("right").value;
movementDir += right*@as(Vec3d, @splat(-walkingSpeed*KeyBoard.key("right").value));
if((Player.super.climbing and !Player.crouching) or onGroundOrFlying) {
movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("right").value;
movementDir += right*@as(Vec3d, @splat(-walkingSpeed*KeyBoard.key("right").value));
}
}
if(KeyBoard.key("jump").pressed) {
if(Player.isFlying.load(.monotonic)) {
Expand All @@ -912,10 +1041,18 @@ pub fn update(deltaTime: f64) void { // MARK: update()
}
Player.jumpCoyote = 0;
}
if(Player.super.climbing) {
climbSpeed = Player.currentClimbSpeed;
climbDir += up;
}
} else {
Player.jumpCooldown = 0;
}
if(KeyBoard.key("fall").pressed) {
if(Player.super.climbing) {
climbSpeed = -Player.currentClimbSpeed;
climbDir += up;
}
if(Player.isFlying.load(.monotonic)) {
if(KeyBoard.key("sprint").pressed) {
if(Player.isGhost.load(.monotonic)) {
Expand All @@ -935,6 +1072,10 @@ pub fn update(deltaTime: f64) void { // MARK: update()
movementDir = vec.normalize(movementDir);
acc += movementDir*@as(Vec3d, @splat(movementSpeed*fricMul));
}
if(climbSpeed != 0 and vec.lengthSquare(climbDir) != 0) {
climbDir = vec.normalize(climbDir);
acc += climbDir*@as(Vec3d, @splat(climbSpeed*fricMul));
}

const newSlot: i32 = @as(i32, @intCast(Player.selectedSlot)) -% @as(i32, @intFromFloat(main.Window.scrollOffset));
Player.selectedSlot = @intCast(@mod(newSlot, 12));
Expand Down Expand Up @@ -1114,32 +1255,35 @@ pub fn update(deltaTime: f64) void { // MARK: update()

const slipLimit = 0.25*Player.currentFriction;

const xMovement = collision.collideOrStep(.client, .x, move[0], Player.super.pos, hitBox, steppingHeight);
Player.super.pos += xMovement;
if(KeyBoard.key("crouch").pressed and Player.onGround and @abs(Player.super.vel[0]) < slipLimit) {
if(collision.collides(.client, .x, 0, Player.super.pos - Vec3d{0, 0, 1}, hitBox) == null) {
Player.super.pos -= xMovement;
Player.super.vel[0] = 0;
var stepAmount: f64 = 0.0;
inline for(0..2) |i| {
const dir: collision.Direction = @enumFromInt(i);
const movement = collision.collideOrStep(.client, dir, move[i], Player.super.pos, hitBox, steppingHeight);
Player.super.pos += movement;
if(KeyBoard.key("crouch").pressed and Player.onGround and @abs(Player.super.vel[i]) < slipLimit) {
if(collision.collides(.client, .x, 0, Player.super.pos - Vec3d{0, 0, 1}, hitBox) == null) blk: {
Player.super.pos -= movement;
Player.super.vel[i] = 0;

// crouch off of blocks onto climbables
if(!Player.super.touchingClimbable) break :blk;
if(collision.collides(.client, dir, 0, Player.super.pos - Vec3d{0, 0, 1}, hitBox)) |box| {
const playerHitboxExtend = (Player.outerBoundingBox.max[i] - Player.outerBoundingBox.min[i])*0.5;
const horizontalDir: f64 = std.math.sign(move[i]);
const offset = horizontalDir*0.005;
const boundingExtend = horizontalDir*playerHitboxExtend;
Player.super.pos[i] = box.max[i] + offset + boundingExtend;
}
}
}
}

const yMovement = collision.collideOrStep(.client, .y, move[1], Player.super.pos, hitBox, steppingHeight);
Player.super.pos += yMovement;
if(KeyBoard.key("crouch").pressed and Player.onGround and @abs(Player.super.vel[1]) < slipLimit) {
if(collision.collides(.client, .y, 0, Player.super.pos - Vec3d{0, 0, 1}, hitBox) == null) {
Player.super.pos -= yMovement;
Player.super.vel[1] = 0;
if(movement[i] != move[i]) {
Player.super.vel[i] = 0;
}
}

if(xMovement[0] != move[0]) {
Player.super.vel[0] = 0;
}
if(yMovement[1] != move[1]) {
Player.super.vel[1] = 0;
stepAmount += movement[2];
}

const stepAmount = xMovement[2] + yMovement[2];
if(stepAmount > 0) {
if(Player.eyeCoyote <= 0) {
Player.eyeVel[2] = @max(1.5*vec.length(Player.super.vel), Player.eyeVel[2], 4);
Expand Down Expand Up @@ -1193,7 +1337,10 @@ pub fn update(deltaTime: f64) void { // MARK: update()
} else if(Player.eyeCoyote > 0) {
Player.eyePos[2] -= move[2];
}
collision.touchBlocks(Player.super, hitBox, .client);

Player.super.climbing = false;
Player.super.touchingClimbable = false;
collision.touchBlocks(&Player.super, hitBox, .client, -13);
} else {
Player.super.pos += move;
}
Expand Down
2 changes: 2 additions & 0 deletions src/server/Entity.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ health: f32 = 8,
maxHealth: f32 = 8,
energy: f32 = 8,
maxEnergy: f32 = 8,
climbing: bool = false,
touchingClimbable: bool = false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Data should only be stored in the client-side player struct for now.

// TODO: Name

pub fn loadFrom(self: *@This(), zon: ZonElement) void {
Expand Down
Loading