Implement AABB collision and add EntityItem properly.

The items are fully functional apart from picking them up, they are commented out in the MPClient breakBlock function if you want to play with them.
This commit is contained in:
Holly Stubbs 2023-11-09 16:30:40 +00:00
parent 6033c86247
commit 63333a04aa
14 changed files with 239 additions and 11 deletions

View File

@ -7,11 +7,15 @@ export default class AABB {
private static readonly aabbPool:FunkyArray<string, AABB> = new FunkyArray<string, AABB>(); private static readonly aabbPool:FunkyArray<string, AABB> = new FunkyArray<string, AABB>();
public readonly aabbPoolString:string; public readonly aabbPoolString:string;
public readonly pooled:boolean;
public initMin:Vec3;
public initMax:Vec3;
public min:Vec3; public min:Vec3;
public max:Vec3; public max:Vec3;
public constructor(minXOrMin:Vec3 | number, minYOrMax:Vec3 | number, minZ?:number, maxX?:number, maxY?:number, maxZ?:number) { public constructor(minXOrMin:Vec3 | number, minYOrMax:Vec3 | number, minZ?:number, maxX?:number, maxY?:number, maxZ?:number, pooled:boolean = false) {
if (minXOrMin instanceof Vec3 && minYOrMax instanceof Vec3) { if (minXOrMin instanceof Vec3 && minYOrMax instanceof Vec3) {
this.min = minXOrMin; this.min = minXOrMin;
this.max = minYOrMax; this.max = minYOrMax;
@ -22,6 +26,11 @@ export default class AABB {
throw new Error("Invalid input parameters: AABB must be supplied with either two Vec3 with the min and max bounds or the raw bounds."); throw new Error("Invalid input parameters: AABB must be supplied with either two Vec3 with the min and max bounds or the raw bounds.");
} }
this.initMin = new Vec3(this.min);
this.initMax = new Vec3(this.max);
this.pooled = pooled;
this.aabbPoolString = AABB.createAABBPoolString(this.min.x, this.min.y, this.min.z, this.max.x, this.max.y, this.max.z); this.aabbPoolString = AABB.createAABBPoolString(this.min.x, this.min.y, this.min.z, this.max.x, this.max.y, this.max.z);
if (!AABB.aabbPool.has(this.aabbPoolString)) { if (!AABB.aabbPool.has(this.aabbPoolString)) {
AABB.aabbPool.set(this.aabbPoolString, this); AABB.aabbPool.set(this.aabbPoolString, this);
@ -36,18 +45,53 @@ export default class AABB {
public static getAABB(minX:number, minY:number, minZ:number, maxX:number, maxY:number, maxZ:number) { public static getAABB(minX:number, minY:number, minZ:number, maxX:number, maxY:number, maxZ:number) {
const aabbPoolString = this.createAABBPoolString(minX, minY, minZ, maxX, maxY, maxZ); const aabbPoolString = this.createAABBPoolString(minX, minY, minZ, maxX, maxY, maxZ);
if (!AABB.aabbPool.has(aabbPoolString)) { if (AABB.aabbPool.has(aabbPoolString)) {
return AABB.aabbPool.get(aabbPoolString); const aabb = AABB.aabbPool.get(aabbPoolString);
if (aabb === undefined) {
throw new Error(`Pooled AABB was ${typeof(aabb)}! This should be impossible.`);
}
return aabb;
} }
return new AABB(minX, minY, minZ, maxX, maxY, maxZ); return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
} }
intersects(a:AABB, b:AABB) { public static intersects(a:AABB, b:AABB) {
return a.min.x <= b.max.x && a.max.x >= b.min.x && a.min.y <= b.max.y && a.max.y >= b.min.y && a.min.z <= b.max.z && a.max.z >= b.min.z; return a.min.x <= b.max.x && a.max.x >= b.min.x && a.min.y <= b.max.y && a.max.y >= b.min.y && a.min.z <= b.max.z && a.max.z >= b.min.z;
} }
intersectionAmount(a:AABB, b:AABB) { public intersects(aabb:AABB) {
return this.min.x <= aabb.max.x && this.max.x >= aabb.min.x && this.min.y <= aabb.max.y && this.max.y >= aabb.min.y && this.min.z <= aabb.max.z && this.max.z >= aabb.min.z;
}
public static intersectionY(a: AABB, b: AABB) {
const minY = Math.max(a.min.y, b.min.y);
const maxY = Math.min(a.max.y, b.max.y);
return minY <= maxY ? maxY - minY : 0;
}
public intersectionY(aabb: AABB) {
const minY = Math.max(this.min.y, aabb.min.y);
const maxY = Math.min(this.max.y, aabb.max.y);
return minY <= maxY ? maxY - minY : 0;
}
public move(xOrVec3:Vec3 | number, y?:number, z?:number) {
if (this.pooled) {
throw new Error(`Attempted to move a pooled AABB. This is not allowed!`);
}
this.min.set(this.initMin);
this.max.set(this.initMax);
if (xOrVec3 instanceof Vec3) {
this.min.add(xOrVec3);
this.max.add(xOrVec3);
} else if (typeof(xOrVec3) === "number" && typeof(y) === "number" && typeof(z) === "number") {
this.min.add(xOrVec3, y, z);
this.max.add(xOrVec3, y, z);
}
} }
} }

View File

@ -22,6 +22,7 @@ import { PacketDisconnectKick } from "./packets/DisconnectKick";
import { ItemStack } from "./inventories/ItemStack"; import { ItemStack } from "./inventories/ItemStack";
import { PacketWindowItems } from "./packets/WindowItems"; import { PacketWindowItems } from "./packets/WindowItems";
import { Block } from "./blocks/Block"; import { Block } from "./blocks/Block";
import { EntityItem } from "./entities/EntityItem";
export class MPClient { export class MPClient {
private readonly mcServer:MinecraftServer; private readonly mcServer:MinecraftServer;
@ -160,6 +161,9 @@ export class MPClient {
this.entity.world.setBlockWithNotify(this.diggingAt.x, this.diggingAt.y, this.diggingAt.z, 0); this.entity.world.setBlockWithNotify(this.diggingAt.x, this.diggingAt.y, this.diggingAt.z, 0);
this.inventory.addItemStack(new ItemStack(Block.blockBehaviours[brokenBlockId].droppedItem(brokenBlockId), 1, metadata)); this.inventory.addItemStack(new ItemStack(Block.blockBehaviours[brokenBlockId].droppedItem(brokenBlockId), 1, metadata));
this.send(new PacketWindowItems(0, this.inventory.getInventorySize(), this.inventory.constructInventoryPayload()).writeData()); this.send(new PacketWindowItems(0, this.inventory.getInventorySize(), this.inventory.constructInventoryPayload()).writeData());
/*const itemEntity = new EntityItem(this.entity.world, new ItemStack(Block.blockBehaviours[brokenBlockId].droppedItem(brokenBlockId), 1, metadata));
itemEntity.position.set(x + 0.5, y + 0.5, z + 0.5);
this.entity.world.addEntity(itemEntity);*/
} }
// TODO: Cap how far away a player is able to break blocks // TODO: Cap how far away a player is able to break blocks

View File

@ -35,7 +35,27 @@ export default class Vec3 {
this.y = y; this.y = y;
this.z = z; this.z = z;
} else { } else {
this.x = this.y = this.z = 0; throw new Error(`Invalid arguments for Vec3.set : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`);
}
}
add(x:Vec3 | number, y?:number, z?:number) {
if (x instanceof Vec3) {
this.set(this.x + x.x, this.y + x.y, this.z + x.z);
} else if (typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number") {
this.set(this.x + x, this.y + y, this.z + z);
} else {
throw new Error(`Invalid arguments for Vec3.add : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`);
}
}
mult(x:Vec3 | number, y?:number, z?:number) {
if (x instanceof Vec3) {
this.set(this.x * x.x, this.y * x.y, this.z * x.z);
} else if (typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number") {
this.set(this.x * x, this.y * y, this.z * z);
} else {
throw new Error(`Invalid arguments for Vec3.mult : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`);
} }
} }
} }

View File

@ -2,6 +2,7 @@ import { FunkyArray } from "../funkyArray";
import { Chunk } from "./Chunk"; import { Chunk } from "./Chunk";
import { WorldSaveManager } from "./WorldSaveManager"; import { WorldSaveManager } from "./WorldSaveManager";
import { Block } from "./blocks/Block"; import { Block } from "./blocks/Block";
import { EntityItem } from "./entities/EntityItem";
import { IEntity } from "./entities/IEntity"; import { IEntity } from "./entities/IEntity";
import { Player } from "./entities/Player"; import { Player } from "./entities/Player";
//import { FlatGenerator } from "./generators/Flat"; //import { FlatGenerator } from "./generators/Flat";
@ -9,6 +10,7 @@ import { HillyGenerator } from "./generators/Hilly";
import { IGenerator } from "./generators/IGenerator"; import { IGenerator } from "./generators/IGenerator";
import { PacketBlockChange } from "./packets/BlockChange"; import { PacketBlockChange } from "./packets/BlockChange";
import { PacketDestroyEntity } from "./packets/DestroyEntity"; import { PacketDestroyEntity } from "./packets/DestroyEntity";
import { PacketPickupSpawn } from "./packets/PickupSpawn";
import { QueuedBlockUpdate } from "./queuedUpdateTypes/BlockUpdate"; import { QueuedBlockUpdate } from "./queuedUpdateTypes/BlockUpdate";
import { IQueuedUpdate } from "./queuedUpdateTypes/IQueuedUpdate"; import { IQueuedUpdate } from "./queuedUpdateTypes/IQueuedUpdate";
@ -46,6 +48,9 @@ export class World {
this.entites.set(entity.entityId, entity); this.entites.set(entity.entityId, entity);
if (entity instanceof Player) { if (entity instanceof Player) {
this.players.set(entity.entityId, entity); this.players.set(entity.entityId, entity);
} else if (entity instanceof EntityItem) {
const packet = new PacketPickupSpawn(entity.entityId, entity.itemStack.itemID, entity.itemStack.size, entity.itemStack.damage, Math.round(entity.position.x * 32), Math.round(entity.position.y * 32), Math.round(entity.position.z * 32), 0, 0, 0).writeData();
entity.sendToNearby(packet);
} }
} }

View File

@ -1,3 +1,4 @@
import AABB from "../AABB";
import { World } from "../World"; import { World } from "../World";
import { BlockBehaviour } from "./BlockBehaviour"; import { BlockBehaviour } from "./BlockBehaviour";
import { BlockBehaviourFlower } from "./BlockBehaviourFlower"; import { BlockBehaviourFlower } from "./BlockBehaviourFlower";
@ -20,6 +21,7 @@ export class Block {
public static readonly blocks:Array<Block> = new Array<Block>(); public static readonly blocks:Array<Block> = new Array<Block>();
public static readonly lightPassage:Array<number> = new Array<number>(); public static readonly lightPassage:Array<number> = new Array<number>();
public static readonly hardness:Array<number> = new Array<number>(); public static readonly hardness:Array<number> = new Array<number>();
public static readonly blockAABBs:Array<AABB> = new Array<AABB>();
public static readonly blockBehaviours:Array<IBlockBehaviour> = new Array<IBlockBehaviour>(); public static readonly blockBehaviours:Array<IBlockBehaviour> = new Array<IBlockBehaviour>();
public static readonly blockNames:Array<string> = new Array<string>(); public static readonly blockNames:Array<string> = new Array<string>();
@ -47,6 +49,14 @@ export class Block {
Block.hardness[this.blockId] = value; Block.hardness[this.blockId] = value;
} }
private get blockAABB() {
return Block.blockAABBs[this.blockId];
}
private set blockAABB(value:AABB) {
Block.blockAABBs[this.blockId] = value;
}
public get blockName() { public get blockName() {
return Block.blockNames[this.blockId]; return Block.blockNames[this.blockId];
} }
@ -108,6 +118,10 @@ export class Block {
return 1 / this.hardness / 100; return 1 / this.hardness / 100;
} }
public getBoundingBox(x:number, y:number, z:number) {
return this.behaviour.getBoundingBox(x, y, z);
}
// Define statics here // Define statics here
static readonly stone = new Block(1).setHardness(1.5).setBehaviour(Behaviour.stone).setBlockName("Stone"); static readonly stone = new Block(1).setHardness(1.5).setBehaviour(Behaviour.stone).setBlockName("Stone");
static readonly grass = new Block(2).setHardness(0.6).setBehaviour(Behaviour.grass).setBlockName("Grass"); static readonly grass = new Block(2).setHardness(0.6).setBehaviour(Behaviour.grass).setBlockName("Grass");

View File

@ -1,7 +1,9 @@
import AABB from "../AABB";
import { World } from "../World"; import { World } from "../World";
import { IBlockBehaviour } from "./IBlockBehaviour"; import { IBlockBehaviour } from "./IBlockBehaviour";
export class BlockBehaviour implements IBlockBehaviour { export class BlockBehaviour implements IBlockBehaviour {
public neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number) {} public neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number) {}
public droppedItem(blockId:number) { return blockId; } public droppedItem(blockId:number) { return blockId; }
public getBoundingBox(x:number, y:number, z:number) { return AABB.getAABB(0 + x, 0 + y, 0 + z, 1 + x, 1 + y, 1 + z); }
} }

View File

@ -1,3 +1,4 @@
import AABB from "../AABB";
import { World } from "../World"; import { World } from "../World";
import { Block } from "./Block"; import { Block } from "./Block";
import { BlockBehaviour } from "./BlockBehaviour"; import { BlockBehaviour } from "./BlockBehaviour";
@ -9,4 +10,8 @@ export class BlockBehaviourFlower extends BlockBehaviour {
world.setBlockWithNotify(x, y, z, 0); world.setBlockWithNotify(x, y, z, 0);
} }
} }
public getBoundingBox() {
return AABB.getAABB(0, 0, 0, 0, 0, 0);
}
} }

View File

@ -1,6 +1,8 @@
import AABB from "../AABB";
import { World } from "../World"; import { World } from "../World";
export interface IBlockBehaviour { export interface IBlockBehaviour {
neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number): void, neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number): void,
droppedItem: (blockId:number) => number droppedItem: (blockId:number) => number,
getBoundingBox: (x:number, y:number, z:number) => AABB,
} }

View File

@ -1,9 +1,11 @@
import AABB from "../AABB";
import { Chunk } from "../Chunk"; import { Chunk } from "../Chunk";
import { MetadataEntry, MetadataWriter } from "../MetadataWriter"; import { MetadataEntry, MetadataWriter } from "../MetadataWriter";
import { Rotation } from "../Rotation"; import { Rotation } from "../Rotation";
import { Vec2 } from "../Vec2"; import { Vec2 } from "../Vec2";
import Vec3 from "../Vec3"; import Vec3 from "../Vec3";
import { World } from "../World"; import { World } from "../World";
import { Block } from "../blocks/Block";
import { MetadataFieldType } from "../enums/MetadataFieldType"; import { MetadataFieldType } from "../enums/MetadataFieldType";
import { PacketEntityLook } from "../packets/EntityLook"; import { PacketEntityLook } from "../packets/EntityLook";
import { PacketEntityLookRelativeMove } from "../packets/EntityLookRelativeMove"; import { PacketEntityLookRelativeMove } from "../packets/EntityLookRelativeMove";
@ -28,6 +30,8 @@ export class Entity implements IEntity {
public lastAbsPosition:Vec3; public lastAbsPosition:Vec3;
public motion:Vec3; public motion:Vec3;
private positionBeforeMove:Vec3;
public rotation:Rotation; public rotation:Rotation;
public lastRotation:Rotation; public lastRotation:Rotation;
public absRotation:Rotation; public absRotation:Rotation;
@ -50,12 +54,18 @@ export class Entity implements IEntity {
private lastCrouchState:boolean; private lastCrouchState:boolean;
private lastFireState:boolean; private lastFireState:boolean;
public entityAABB:AABB;
private readonly isPlayer:boolean;
private queuedChunkUpdate:boolean; private queuedChunkUpdate:boolean;
public constructor(world:World) { public constructor(world:World, isPlayer:boolean = false) {
this.entityId = Entity.nextEntityId++; this.entityId = Entity.nextEntityId++;
this.isPlayer = isPlayer;
this.entitySize = new Vec2(0.6, 1.8); this.entitySize = new Vec2(0.6, 1.8);
this.entityAABB = new AABB(-this.entitySize.x / 2, 0, -this.entitySize.x / 2, this.entitySize.x / 2, this.entitySize.y, this.entitySize.x / 2);
this.fire = this.fallDistance = 0; this.fire = this.fallDistance = 0;
this.onGround = false; this.onGround = false;
@ -68,6 +78,8 @@ export class Entity implements IEntity {
this.lastAbsPosition = new Vec3(); this.lastAbsPosition = new Vec3();
this.motion = new Vec3(); this.motion = new Vec3();
this.positionBeforeMove = new Vec3();
this.rotation = new Rotation(); this.rotation = new Rotation();
this.lastRotation = new Rotation(); this.lastRotation = new Rotation();
this.absRotation = new Rotation(); this.absRotation = new Rotation();
@ -201,6 +213,25 @@ export class Entity implements IEntity {
} }
} }
moveEntity(motionX:number, motionY:number, motionZ:number) {
this.positionBeforeMove.set(this.position);
const blockId = this.chunk.getBlockId(Math.floor(this.positionBeforeMove.x) & 0xf, Math.floor(this.positionBeforeMove.y), Math.floor(this.positionBeforeMove.z) & 0xf);
const blockUnderEntity = blockId > 0 ? Block.blocks[blockId] : null;
this.position.add(motionX, motionY, motionZ);
this.entityAABB.move(this.position);
if (blockUnderEntity !== null) {
const blockBoundingBox = blockUnderEntity.getBoundingBox(Math.floor(this.positionBeforeMove.x), Math.floor(this.positionBeforeMove.y), Math.floor(this.positionBeforeMove.z));
if (this.entityAABB.intersects(blockBoundingBox)) {
const intersection = this.entityAABB.intersectionY(blockBoundingBox);
this.position.add(0, intersection, 0);
this.motion.y = 0;
this.onGround = true;
}
}
}
onTick() { onTick() {
this.updateMetadata(); this.updateMetadata();
this.updateEntityChunk(); this.updateEntityChunk();

View File

@ -6,12 +6,47 @@ export class EntityItem extends Entity {
public age:number; public age:number;
public itemStack:ItemStack; public itemStack:ItemStack;
public pickupDelay:number;
public constructor(world:World, itemStack:ItemStack) { public constructor(world:World, itemStack:ItemStack) {
super(world); super(world);
this.itemStack = itemStack; this.itemStack = itemStack;
this.entitySize.set(0.2, 0.2);
this.pickupDelay = 0;
this.motion.set(Math.random() * 0.2 - 0.1, 0.2, Math.random() * 0.2 - 0.1);
this.age = 0; this.age = 0;
this.health = 5; this.health = 5;
} }
onTick() {
super.onTick();
if (this.pickupDelay > 0) {
this.pickupDelay--;
}
this.motion.add(0, -0.04, 0);
this.moveEntity(this.motion.x, this.motion.y, this.motion.z);
let xyMult = 0.98;
if (this.onGround) {
xyMult = 0.59;
}
// TODO: Change the x and z based on the slipperiness of a block
this.motion.mult(xyMult, 0.98, xyMult);
if (this.onGround) {
this.motion.y *= -0.5;
}
this.age++;
if (this.age >= 6000) {
// TODO: Kill entity
}
}
} }

View File

@ -18,8 +18,8 @@ export class EntityLiving extends Entity {
public headHeight:number; public headHeight:number;
public lastHealth:number; public lastHealth:number;
public constructor(world:World) { public constructor(world:World, isPlayer:boolean = false) {
super(world); super(world, isPlayer);
this.timeInWater = 0; this.timeInWater = 0;
this.headHeight = 1.62; this.headHeight = 1.62;

View File

@ -27,7 +27,7 @@ export class Player extends EntityLiving {
public trackedEquipment:Array<ItemStack | null>; public trackedEquipment:Array<ItemStack | null>;
public constructor(server:MinecraftServer, world:World, username:string) { public constructor(server:MinecraftServer, world:World, username:string) {
super(world); super(world, true);
this.server = server; this.server = server;
this.firstUpdate = true; this.firstUpdate = true;
this.loadedChunks = new Array<number>(); this.loadedChunks = new Array<number>();
@ -140,6 +140,9 @@ export class Player extends EntityLiving {
// Calculate player motion since we don't have it serverside. // Calculate player motion since we don't have it serverside.
this.motion.set(this.position.x - this.lastPosition.x, this.position.y - this.lastPosition.y, this.position.z - this.lastPosition.z); this.motion.set(this.position.x - this.lastPosition.x, this.position.y - this.lastPosition.y, this.position.z - this.lastPosition.z);
if (!this.motion.isZero) {
this.entityAABB.move(this.position);
}
super.onTick(); super.onTick();

View File

@ -21,6 +21,7 @@ export enum Packet {
Animation = 0x12, Animation = 0x12,
EntityAction = 0x13, EntityAction = 0x13,
NamedEntitySpawn = 0x14, NamedEntitySpawn = 0x14,
PickupSpawn = 0x15,
EntityVelocity = 0x1C, EntityVelocity = 0x1C,
DestroyEntity = 0x1D, DestroyEntity = 0x1D,

View File

@ -0,0 +1,62 @@
import { createWriter, IReader, Endian } from "bufferstuff";
import { IPacket } from "./IPacket";
import { Packet } from "../enums/Packet";
export class PacketPickupSpawn implements IPacket {
public packetId = Packet.PickupSpawn;
public entityId:number;
public item:number;
public count:number;
public damage:number;
public x:number;
public y:number;
public z:number;
public yaw:number;
public pitch:number;
public roll:number;
public constructor(entityId?:number, item?:number, count?:number, damage?:number, x?:number, y?:number, z?:number, yaw?:number, pitch?:number, roll?:number) {
if (typeof(entityId) === "number" && typeof(item) === "number" && typeof(count) === "number" && typeof(damage) === "number" && typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number" && typeof(yaw) === "number" && typeof(pitch) === "number" && typeof(roll) === "number") {
this.entityId = entityId;
this.item = item;
this.count = count;
this.damage = damage;
this.x = x;
this.y = y;
this.z = z;
this.yaw = yaw;
this.pitch = pitch;
this.roll = roll;
} else {
this.entityId = Number.MIN_VALUE;
this.item = Number.MIN_VALUE;
this.count = Number.MIN_VALUE;
this.damage = Number.MIN_VALUE;
this.x = Number.MIN_VALUE;
this.y = Number.MIN_VALUE;
this.z = Number.MIN_VALUE;
this.yaw = Number.MIN_VALUE;
this.pitch = Number.MIN_VALUE;
this.roll = Number.MIN_VALUE;
}
}
public readData(reader:IReader) {
this.entityId = reader.readInt();
this.item = reader.readShort();
this.count = reader.readByte();
this.damage = reader.readShort();
this.x = reader.readInt();
this.y = reader.readInt();
this.z = reader.readInt();
this.yaw = reader.readByte();
this.pitch = reader.readByte();
this.roll = reader.readByte();
return this;
}
public writeData() {
return createWriter(Endian.BE, 25).writeUByte(this.packetId).writeInt(this.entityId).writeShort(this.item).writeByte(this.count).writeShort(this.damage).writeInt(this.x).writeInt(this.y).writeInt(this.z).writeByte(this.yaw).writeByte(this.pitch).writeByte(this.roll).toBuffer();
}
}