ecs refactor
This commit is contained in:
parent
28b026a614
commit
f2e5d13000
8 changed files with 410 additions and 170 deletions
200
server/engine.ts
200
server/engine.ts
|
@ -1,123 +1,92 @@
|
|||
import { assertEquals } from "https://deno.land/std@0.221.0/assert/mod.ts";
|
||||
import { chunks, enumerate, log_from, Prototyped, range, Vec2 } from "../common/utils.ts";
|
||||
import { ClientInterface } from "./network.ts";
|
||||
import { Session } from "./entities/player.ts";
|
||||
import { Awaitable, log_from, Prototyped, Vec2 } from "../common/utils.ts";
|
||||
import { ClassMap } from "../common/utils.ts";
|
||||
import { maybe } from "../common/utils.ts";
|
||||
import { wait } from "../common/utils.ts";
|
||||
import { Constructible } from "../common/utils.ts";
|
||||
import { first } from "../common/utils.ts";
|
||||
const log = log_from(import.meta);
|
||||
|
||||
export class WorldEntity {
|
||||
display;
|
||||
world;
|
||||
export class Entity {
|
||||
identifier;
|
||||
position;
|
||||
compontents;
|
||||
components;
|
||||
engine;
|
||||
|
||||
constructor(world: World, identifier: number, display: string, position: Vec2, ...components: Prototyped[]) {
|
||||
this.display = display;
|
||||
this.world = world;
|
||||
constructor(identifier: number, engine: Engine) {
|
||||
this.identifier = identifier;
|
||||
this.position = position;
|
||||
this.compontents = new ClassMap();
|
||||
for (const c of components) this.compontents.insert(c);
|
||||
}
|
||||
}
|
||||
|
||||
export class World {
|
||||
next_id;
|
||||
entities;
|
||||
|
||||
constructor() {
|
||||
this.next_id = 0;
|
||||
this.entities = new Map<number, WorldEntity>();
|
||||
this.engine = engine;
|
||||
this.components = new ClassMap();
|
||||
}
|
||||
|
||||
spawn_entity(display: string, pos: Vec2) {
|
||||
const entity = new WorldEntity(this, this.next_id++, display, pos);
|
||||
this.entities.set(entity.identifier, entity);
|
||||
return entity;
|
||||
insert(...components: Prototyped[]) {
|
||||
for (const c of components) this.components.insert(c);
|
||||
return this;
|
||||
}
|
||||
|
||||
async spawn_structure(file_path: string, origin: Vec2) {
|
||||
const content = await Deno.readTextFile(file_path);
|
||||
let count = 0;
|
||||
for (const [y, line] of enumerate(content.split("\n"))) {
|
||||
for (const [x, chars] of enumerate(chunks(line, 2))) {
|
||||
const display = chars[0] + chars[1];
|
||||
if (display === " ") continue;
|
||||
this.spawn_entity(display, origin.add(new Vec2(x, y)));
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
log("Spawned", count, "tiles from", file_path);
|
||||
get<I>(class_: Constructible<I>) {
|
||||
return this.components.get(class_);
|
||||
}
|
||||
|
||||
render(center: Vec2, size: Vec2) {
|
||||
const radius = size.scale(0.5);
|
||||
const result = Array.from(range(0, size.y())).map(() => Array.from(range(0, size.x())).map(() => " "));
|
||||
const top_left = center.sub(radius);
|
||||
const bottom_right = center.add(radius);
|
||||
for (const entity of this.entities.values()) {
|
||||
if (!entity.position.inside(top_left, bottom_right)) continue;
|
||||
const local_pos = entity.position.sub(top_left);
|
||||
result[local_pos.y()][local_pos.x()] = entity.display;
|
||||
}
|
||||
return result.map((line) => line.join("")).toReversed().join("\n");
|
||||
}
|
||||
|
||||
move_collide(entity_id: number, displacement: Vec2) {
|
||||
let result = false;
|
||||
const entity = this.get_entity(entity_id);
|
||||
if (entity === undefined) return result;
|
||||
for (const step of displacement_steps(entity.position, displacement)) {
|
||||
const collision = this.get_entity_at(step);
|
||||
if (collision !== undefined) return result;
|
||||
entity.position = step;
|
||||
result = true;
|
||||
}
|
||||
get_force<I>(class_: Constructible<I>) {
|
||||
const result = this.get(class_);
|
||||
if (result === null) throw new Error();
|
||||
return result;
|
||||
}
|
||||
|
||||
get_entity(entity_id: number) {
|
||||
return this.entities.get(entity_id);
|
||||
}
|
||||
|
||||
*get_entities_at(pos: Vec2) {
|
||||
for (const entity of this.entities.values()) if (entity.position.overlaps(pos)) yield entity;
|
||||
}
|
||||
|
||||
get_entity_at(pos: Vec2) {
|
||||
return maybe([...this.get_entities_at(pos)][0]);
|
||||
}
|
||||
|
||||
*get_entities_in_range(min: Vec2, max: Vec2) {
|
||||
for (const entity of this.entities.values()) if (entity.position.inside(min, max)) yield entity;
|
||||
}
|
||||
|
||||
get_entity_in_range(min: Vec2, max: Vec2) {
|
||||
return maybe([...this.get_entities_in_range(min, max)][0]);
|
||||
}
|
||||
|
||||
kill(entity_id: number) {
|
||||
this.entities.delete(entity_id);
|
||||
}
|
||||
}
|
||||
|
||||
export class Engine {
|
||||
world;
|
||||
next_identifier;
|
||||
entities;
|
||||
|
||||
constructor(world: World) {
|
||||
this.world = world;
|
||||
constructor() {
|
||||
this.next_identifier = 0;
|
||||
this.entities = new Map<number, Entity>();
|
||||
}
|
||||
|
||||
async start() {
|
||||
start() {
|
||||
log("Started engine.");
|
||||
}
|
||||
|
||||
add_session(client: ClientInterface) {
|
||||
const player_entity = this.world.spawn_entity("`/", new Vec2(0, 0));
|
||||
const session = new Session(client, this.world, player_entity);
|
||||
session.spin();
|
||||
spawn(op: (entity: Entity) => unknown) {
|
||||
const identifier = this.next_identifier++;
|
||||
const entity = new Entity(identifier, this);
|
||||
op(entity);
|
||||
this.entities.set(identifier, entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
run<T>(sys: (engine: Engine) => T) {
|
||||
return sys(this);
|
||||
}
|
||||
|
||||
async global_system_loop(sys: (engine: Engine) => Awaitable, interval: number) {
|
||||
while (true) {
|
||||
await sys(this);
|
||||
await wait(interval);
|
||||
}
|
||||
}
|
||||
|
||||
*query_all(query: Query) {
|
||||
yield* query.run(this.entities.values());
|
||||
}
|
||||
|
||||
query_one(query: Query) {
|
||||
return first(this.query_all(query));
|
||||
}
|
||||
|
||||
get(identifier: number) {
|
||||
const value = this.entities.get(identifier);
|
||||
if (value === undefined) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
get_force(identifier: number) {
|
||||
const entity = this.get(identifier);
|
||||
if (entity === null) throw new Error();
|
||||
return entity;
|
||||
}
|
||||
|
||||
delete(identifier: number) {
|
||||
return this.entities.delete(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,3 +120,42 @@ Deno.test("test_displacement", () => {
|
|||
[new Vec2(2, 2), new Vec2(3, 3), new Vec2(4, 4), new Vec2(5, 5), new Vec2(5, 6), new Vec2(5, 7)],
|
||||
);
|
||||
});
|
||||
|
||||
export class Query {
|
||||
private test;
|
||||
|
||||
constructor(test: (entity: Entity) => boolean) {
|
||||
this.test = test;
|
||||
}
|
||||
|
||||
and(query: Query) {
|
||||
return new Query((entity) => this.test(entity) && query.test(entity));
|
||||
}
|
||||
|
||||
static with(...components: Constructible[]) {
|
||||
return new Query((entity) => {
|
||||
for (const comp of components) if (!entity.components.has(comp)) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
with(...components: Constructible[]) {
|
||||
return this.and(Query.with(...components));
|
||||
}
|
||||
|
||||
static filter<I>(component: Constructible<I>, filter: (comp: I, entity: Entity) => boolean) {
|
||||
return new Query((entity) => {
|
||||
const comp = entity.get(component);
|
||||
if (comp === null) return false;
|
||||
return filter(comp, entity);
|
||||
});
|
||||
}
|
||||
|
||||
filter<I>(component: Constructible<I>, filter: (comp: I, entity: Entity) => boolean) {
|
||||
return this.and(Query.filter(component, filter));
|
||||
}
|
||||
|
||||
*run(entities: Iterable<Entity>) {
|
||||
for (const entity of entities) if (this.test(entity)) yield entity;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue