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>();
public readonly aabbPoolString:string;
public readonly pooled:boolean;
public initMin:Vec3;
public initMax:Vec3;
public min: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) {
this.min = minXOrMin;
this.max = minYOrMax;
@ -21,6 +25,11 @@ export default class AABB {
} else {
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);
if (!AABB.aabbPool.has(this.aabbPoolString)) {
@ -36,18 +45,53 @@ export default class AABB {
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);
if (!AABB.aabbPool.has(aabbPoolString)) {
return AABB.aabbPool.get(aabbPoolString);
if (AABB.aabbPool.has(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);
}
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;
}
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 { PacketWindowItems } from "./packets/WindowItems";
import { Block } from "./blocks/Block";
import { EntityItem } from "./entities/EntityItem";
export class MPClient {
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.inventory.addItemStack(new ItemStack(Block.blockBehaviours[brokenBlockId].droppedItem(brokenBlockId), 1, metadata));
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

View file

@ -35,7 +35,27 @@ export default class Vec3 {
this.y = y;
this.z = z;
} 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 { WorldSaveManager } from "./WorldSaveManager";
import { Block } from "./blocks/Block";
import { EntityItem } from "./entities/EntityItem";
import { IEntity } from "./entities/IEntity";
import { Player } from "./entities/Player";
//import { FlatGenerator } from "./generators/Flat";
@ -9,6 +10,7 @@ import { HillyGenerator } from "./generators/Hilly";
import { IGenerator } from "./generators/IGenerator";
import { PacketBlockChange } from "./packets/BlockChange";
import { PacketDestroyEntity } from "./packets/DestroyEntity";
import { PacketPickupSpawn } from "./packets/PickupSpawn";
import { QueuedBlockUpdate } from "./queuedUpdateTypes/BlockUpdate";
import { IQueuedUpdate } from "./queuedUpdateTypes/IQueuedUpdate";
@ -46,6 +48,9 @@ export class World {
this.entites.set(entity.entityId, entity);
if (entity instanceof Player) {
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 { BlockBehaviour } from "./BlockBehaviour";
import { BlockBehaviourFlower } from "./BlockBehaviourFlower";
@ -20,6 +21,7 @@ export class Block {
public static readonly blocks:Array<Block> = new Array<Block>();
public static readonly lightPassage: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 blockNames:Array<string> = new Array<string>();
@ -47,6 +49,14 @@ export class Block {
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() {
return Block.blockNames[this.blockId];
}
@ -107,6 +117,10 @@ export class Block {
// TODO: Have the 1 be based on current tool ig
return 1 / this.hardness / 100;
}
public getBoundingBox(x:number, y:number, z:number) {
return this.behaviour.getBoundingBox(x, y, z);
}
// Define statics here
static readonly stone = new Block(1).setHardness(1.5).setBehaviour(Behaviour.stone).setBlockName("Stone");

View file

@ -1,7 +1,9 @@
import AABB from "../AABB";
import { World } from "../World";
import { IBlockBehaviour } from "./IBlockBehaviour";
export class BlockBehaviour implements IBlockBehaviour {
public neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number) {}
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 { Block } from "./Block";
import { BlockBehaviour } from "./BlockBehaviour";
@ -9,4 +10,8 @@ export class BlockBehaviourFlower extends BlockBehaviour {
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";
export interface IBlockBehaviour {
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 { MetadataEntry, MetadataWriter } from "../MetadataWriter";
import { Rotation } from "../Rotation";
import { Vec2 } from "../Vec2";
import Vec3 from "../Vec3";
import { World } from "../World";
import { Block } from "../blocks/Block";
import { MetadataFieldType } from "../enums/MetadataFieldType";
import { PacketEntityLook } from "../packets/EntityLook";
import { PacketEntityLookRelativeMove } from "../packets/EntityLookRelativeMove";
@ -28,6 +30,8 @@ export class Entity implements IEntity {
public lastAbsPosition:Vec3;
public motion:Vec3;
private positionBeforeMove:Vec3;
public rotation:Rotation;
public lastRotation:Rotation;
public absRotation:Rotation;
@ -50,12 +54,18 @@ export class Entity implements IEntity {
private lastCrouchState:boolean;
private lastFireState:boolean;
public entityAABB:AABB;
private readonly isPlayer:boolean;
private queuedChunkUpdate:boolean;
public constructor(world:World) {
public constructor(world:World, isPlayer:boolean = false) {
this.entityId = Entity.nextEntityId++;
this.isPlayer = isPlayer;
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.onGround = false;
@ -68,6 +78,8 @@ export class Entity implements IEntity {
this.lastAbsPosition = new Vec3();
this.motion = new Vec3();
this.positionBeforeMove = new Vec3();
this.rotation = new Rotation();
this.lastRotation = 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() {
this.updateMetadata();
this.updateEntityChunk();

View file

@ -6,12 +6,47 @@ export class EntityItem extends Entity {
public age:number;
public itemStack:ItemStack;
public pickupDelay:number;
public constructor(world:World, itemStack:ItemStack) {
super(world);
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.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 lastHealth:number;
public constructor(world:World) {
super(world);
public constructor(world:World, isPlayer:boolean = false) {
super(world, isPlayer);
this.timeInWater = 0;
this.headHeight = 1.62;

View file

@ -27,7 +27,7 @@ export class Player extends EntityLiving {
public trackedEquipment:Array<ItemStack | null>;
public constructor(server:MinecraftServer, world:World, username:string) {
super(world);
super(world, true);
this.server = server;
this.firstUpdate = true;
this.loadedChunks = new Array<number>();
@ -140,6 +140,9 @@ export class Player extends EntityLiving {
// 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);
if (!this.motion.isZero) {
this.entityAABB.move(this.position);
}
super.onTick();

View file

@ -21,6 +21,7 @@ export enum Packet {
Animation = 0x12,
EntityAction = 0x13,
NamedEntitySpawn = 0x14,
PickupSpawn = 0x15,
EntityVelocity = 0x1C,
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();
}
}