meat/engine/world/Level.ts

124 lines
3.7 KiB
TypeScript
Raw Normal View History

import {
Actor,
type Actor as RuntimeActor,
} from "../scene/Actor"
import { Collider, type Collider as CollisionShape } from "./Collider"
import {
CollisionWorld,
type CollisionWorld as Collision,
} from "./CollisionWorld"
import type { Terrain } from "./Terrain"
import {
RenderScene,
type Billboard,
type RenderInstance,
type RenderPrototype,
type RenderScene as Scene,
} from "../render/RenderScene"
import type { Chunk } from "../render/Chunk"
import type { DrawGroup } from "../render/Material"
import type { SkyConfig } from "../render/Sky"
import type { Mat4 } from "../math/Mat4"
import type { Vec3 } from "../math/Vec3"
export type LevelDefinition<World> = {
terrain: Terrain
actorWorld: World
actors: readonly RuntimeActor<World>[]
staticColliders: CollisionShape[]
staticGroups: DrawGroup[]
chunks: Chunk[]
billboards: Billboard[]
sky: SkyConfig
}
/** Live engine world. Behavior-bearing actors stay here on the main thread; only
* `render` is clone-safe and sent to workers. */
export type Level<World> = {
readonly terrain: Terrain
readonly actorWorld: World
readonly actors: readonly RuntimeActor<World>[]
readonly collision: Collision
readonly render: Scene
}
const prototypeIndexes = new WeakMap<object, ReadonlyMap<object, number>>()
export namespace Level {
export function create<World>(definition: LevelDefinition<World>): Level<World> {
const actors = Object.freeze([...definition.actors])
const prototypes: RenderPrototype[] = []
const prototypeIndex = new Map<object, number>()
for (const actor of actors) {
if (!prototypeIndex.has(actor.definition)) {
prototypeIndex.set(actor.definition, prototypes.length)
prototypes.push(actor.prototype)
}
}
const level: Level<World> = {
terrain: definition.terrain,
actorWorld: definition.actorWorld,
actors,
collision: CollisionWorld.create(
definition.terrain,
definition.staticColliders,
),
render: Object.freeze({
chunks: Object.freeze([...definition.chunks]),
staticGroups: Object.freeze([...definition.staticGroups]),
billboards: Object.freeze([...definition.billboards]),
prototypes: Object.freeze(prototypes),
maxInstances: actors.length,
sky: definition.sky,
}),
}
prototypeIndexes.set(level, prototypeIndex)
return level
}
export function update<World>(level: Level<World>, dt: number): void {
for (const actor of level.actors) {
Actor.update(actor, dt, level.actorWorld)
}
}
export function visibleInstances<World>(
level: Level<World>,
viewProjection: Mat4,
): RenderInstance[] {
const prototypeIndex = prototypeIndexes.get(level)
if (prototypeIndex === undefined) {
throw new Error("level was not created by Level.create")
}
const instances: RenderInstance[] = []
for (const actor of level.actors) {
const prototype = prototypeIndex.get(actor.definition)
if (prototype !== undefined) {
instances.push({ prototype, ...Actor.transform(actor) })
}
}
return RenderScene.visibleInstances(level.render, instances, viewProjection)
}
export function refreshActorColliders<World>(
level: Level<World>,
focus: Vec3,
range: number,
): void {
const dynamic = level.collision.dynamicColliders
dynamic.length = 0
const rangeSquared = range * range
for (const actor of level.actors) {
const collider = Actor.collider(actor, level.actorWorld)
if (collider === null) {
continue
}
const dx = Collider.centerX(collider) - focus.x
const dz = Collider.centerZ(collider) - focus.z
if (dx * dx + dz * dz <= rangeSquared) {
dynamic.push(collider)
}
}
}
}