refactor: move world concepts into engine
This commit is contained in:
parent
eeedcb8e48
commit
2d15c7ab8d
52 changed files with 3298 additions and 1558 deletions
|
|
@ -1,26 +1,48 @@
|
|||
import type { Mesh } from "./Mesh"
|
||||
import type { RenderPrototype, RenderTransform } from "../render/RenderScene"
|
||||
import type { Collider } from "../world/Collider"
|
||||
|
||||
/** Definition of an **Entity** actor kind: something that lives in the world with
|
||||
* its own behavior and a live transform (mobs, and later the npc / powerups) -- as
|
||||
* opposed to a baked, static `Prop`. `State` is the per-instance runtime record the
|
||||
* behavior mutates; `World` is whatever that behavior reads (e.g. `Terrain`).
|
||||
*
|
||||
* Every field is plain data or a module function, so a definition is **imported
|
||||
* into each context** (main thread + each render worker) rather than structured-
|
||||
* cloned across the wire -- the per-kind polymorphism is code, not serialized
|
||||
* state. That's what lets a registry of these stay compatible with the worker
|
||||
* renderer (only plain instance data ever crosses; behavior is loaded per side). */
|
||||
export type Entity<State, World> = {
|
||||
/** Stable tag for the kind (also the texture key today). The registry's order,
|
||||
* not this string, is what becomes the id packed into the mob SAB. */
|
||||
name: string
|
||||
/** Build the canonical local-space mesh once; every instance shares it, differing
|
||||
* only by its per-frame model matrix. */
|
||||
build: (mesh: Mesh) => void
|
||||
/** Advance one instance by `dt` seconds. */
|
||||
/** Open actor behavior and representation. Concrete definitions are ordinary game
|
||||
* objects referenced directly by instances. */
|
||||
export type ActorDefinition<State, World> = {
|
||||
prototype: RenderPrototype
|
||||
update: (state: State, dt: number, world: World) => void
|
||||
/** Local bounding radius (pre-scale) for the per-frame cull AABB. */
|
||||
boundingRadius: number
|
||||
/** Local body height (pre-scale) for the top of the stand-on collider. */
|
||||
bodyHeight: number
|
||||
transform: (state: State) => RenderTransform
|
||||
collider?: (state: State, world: World) => Collider | null
|
||||
}
|
||||
|
||||
/** Type-erased live actor. `create` captures concrete state safely, allowing one
|
||||
* level to hold unrelated actor state types without a content union. */
|
||||
export type Actor<World> = {
|
||||
readonly definition: object
|
||||
readonly prototype: RenderPrototype
|
||||
readonly updateState: (dt: number, world: World) => void
|
||||
readonly readTransform: () => RenderTransform
|
||||
readonly readCollider: (world: World) => Collider | null
|
||||
}
|
||||
|
||||
export namespace Actor {
|
||||
export function create<State, World>(
|
||||
definition: ActorDefinition<State, World>,
|
||||
state: State,
|
||||
): Actor<World> {
|
||||
return {
|
||||
definition,
|
||||
prototype: definition.prototype,
|
||||
updateState: (dt, world) => definition.update(state, dt, world),
|
||||
readTransform: () => definition.transform(state),
|
||||
readCollider: (world) => definition.collider?.(state, world) ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export function update<World>(actor: Actor<World>, dt: number, world: World): void {
|
||||
actor.updateState(dt, world)
|
||||
}
|
||||
|
||||
export function transform<World>(actor: Actor<World>): RenderTransform {
|
||||
return actor.readTransform()
|
||||
}
|
||||
|
||||
export function collider<World>(actor: Actor<World>, world: World): Collider | null {
|
||||
return actor.readCollider(world)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
80
engine/scene/MeshBuilder.ts
Normal file
80
engine/scene/MeshBuilder.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { STRIDE, type Mesh } from "./Mesh"
|
||||
|
||||
type Corner = [number, number, number]
|
||||
|
||||
export namespace MeshBuilder {
|
||||
export function quad(
|
||||
mesh: Mesh,
|
||||
a: Corner,
|
||||
b: Corner,
|
||||
c: Corner,
|
||||
d: Corner,
|
||||
uScale: number,
|
||||
vScale: number,
|
||||
): void {
|
||||
const base = mesh.verts.length / STRIDE
|
||||
mesh.verts.push(
|
||||
a[0],
|
||||
a[1],
|
||||
a[2],
|
||||
0,
|
||||
0,
|
||||
b[0],
|
||||
b[1],
|
||||
b[2],
|
||||
uScale,
|
||||
0,
|
||||
c[0],
|
||||
c[1],
|
||||
c[2],
|
||||
uScale,
|
||||
vScale,
|
||||
d[0],
|
||||
d[1],
|
||||
d[2],
|
||||
0,
|
||||
vScale,
|
||||
)
|
||||
mesh.indices.push(base, base + 1, base + 2, base, base + 2, base + 3)
|
||||
}
|
||||
|
||||
export function slab(
|
||||
mesh: Mesh,
|
||||
x0: number,
|
||||
x1: number,
|
||||
z0: number,
|
||||
z1: number,
|
||||
y0: number,
|
||||
y1: number,
|
||||
tilesPerUnit: number,
|
||||
): void {
|
||||
const dx = (x1 - x0) * tilesPerUnit
|
||||
const dz = (z1 - z0) * tilesPerUnit
|
||||
const dy = (y1 - y0) * tilesPerUnit
|
||||
quad(mesh, [x0, y1, z0], [x1, y1, z0], [x1, y1, z1], [x0, y1, z1], dx, dz)
|
||||
quad(mesh, [x0, y0, z0], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0], dx, dy)
|
||||
quad(mesh, [x1, y0, z1], [x0, y0, z1], [x0, y1, z1], [x1, y1, z1], dx, dy)
|
||||
quad(mesh, [x0, y0, z1], [x0, y0, z0], [x0, y1, z0], [x0, y1, z1], dz, dy)
|
||||
quad(mesh, [x1, y0, z0], [x1, y0, z1], [x1, y1, z1], [x1, y1, z0], dz, dy)
|
||||
}
|
||||
|
||||
export function box(
|
||||
mesh: Mesh,
|
||||
centerX: number,
|
||||
centerZ: number,
|
||||
half: number,
|
||||
base: number,
|
||||
height: number,
|
||||
): void {
|
||||
const x0 = centerX - half
|
||||
const x1 = centerX + half
|
||||
const z0 = centerZ - half
|
||||
const z1 = centerZ + half
|
||||
const y1 = base + height
|
||||
quad(mesh, [x0, y1, z0], [x1, y1, z0], [x1, y1, z1], [x0, y1, z1], 1, 1)
|
||||
quad(mesh, [x0, base, z0], [x1, base, z0], [x1, y1, z0], [x0, y1, z0], 1, 1)
|
||||
quad(mesh, [x1, base, z1], [x0, base, z1], [x0, y1, z1], [x1, y1, z1], 1, 1)
|
||||
quad(mesh, [x1, base, z0], [x1, base, z1], [x1, y1, z1], [x1, y1, z0], 1, 1)
|
||||
quad(mesh, [x0, base, z1], [x0, base, z0], [x0, y1, z0], [x0, y1, z1], 1, 1)
|
||||
}
|
||||
}
|
||||
38
engine/scene/Prefab.ts
Normal file
38
engine/scene/Prefab.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { Vec3 } from "../math/Vec3"
|
||||
import type { MeshBatch } from "../render/ChunkBuilder"
|
||||
import type { Collider } from "../world/Collider"
|
||||
|
||||
/** Static content recipe. Concrete game prefabs implement this engine contract and
|
||||
* are referenced as objects, never through a closed content-kind registry. */
|
||||
export type Prefab<State> = {
|
||||
position: (state: State) => Vec3
|
||||
bakeNear: (state: State, batch: MeshBatch) => void
|
||||
bakeFar?: (state: State, batch: MeshBatch) => void
|
||||
collider?: (state: State) => Collider | null
|
||||
}
|
||||
|
||||
/** Type-erased placed prefab consumed during level compilation only. */
|
||||
export type PlacedPrefab = {
|
||||
position: Vec3
|
||||
bakeNear: (batch: MeshBatch) => void
|
||||
bakeFar?: (batch: MeshBatch) => void
|
||||
collider: Collider | null
|
||||
}
|
||||
|
||||
export namespace Prefab {
|
||||
export function place<State>(
|
||||
definition: Prefab<State>,
|
||||
state: State,
|
||||
): PlacedPrefab {
|
||||
const bakeFar = definition.bakeFar
|
||||
return {
|
||||
position: definition.position(state),
|
||||
bakeNear: (batch) => definition.bakeNear(state, batch),
|
||||
bakeFar:
|
||||
bakeFar === undefined
|
||||
? undefined
|
||||
: (batch) => bakeFar(state, batch),
|
||||
collider: definition.collider?.(state) ?? null,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue