495 lines
20 KiB
Zig
495 lines
20 KiB
Zig
// wew lad. this is definitely an exploratory implementation I think.
|
|
//
|
|
// todo: cruft
|
|
// - componentregistration.zig should contain all the code having to do with lua registration
|
|
// - entities should be under ecs/Entities.zig
|
|
//
|
|
// ECS: Entities, Containers, Systems
|
|
//
|
|
// So this is going to be a bit different thatn flecs or other more mainstream systems. You should think of
|
|
// this ecs system as more of a way to implement systems which can talk to each other on a level playing ground.
|
|
//
|
|
// My world view of how game systems should talk to each other is more like small processes, and the game state
|
|
// should be a collection of tiny databases that each contain discrete bits of information that piece together
|
|
// to produce a coherent vision of the world.
|
|
//
|
|
// Everything that should be tracked through ECS should be something that is gameplay significant.
|
|
// To this end, ecs has a maximum entity count of 1 million unique entities.
|
|
//
|
|
// This should be decent enough to pretty much implement 99% of features in th ecs features.
|
|
//
|
|
// However features which require a large amount of entities is likely a bad idea and not something that
|
|
// should be done in ecs.
|
|
//
|
|
// Eg. you wouldn't do a particle system in ecs and have each transform be a unique entry in the transforms
|
|
// sparse set.
|
|
//
|
|
// But you could conceivably do projectiles.
|
|
//
|
|
// Overall though you need to ask yourself. If something needs to be an entity or not. entities exist to
|
|
// recieve messages. Not nessecarily even send messages.
|
|
//
|
|
// The base implementation ideas are mostly solid but the level of cruft involed in scripting and ecs is a bit
|
|
// nasty at this time bear with me.
|
|
//
|
|
// Ok one mental model I think I can with entities is that each entity can be thought of as an independent object
|
|
// that contains no state.
|
|
//
|
|
// When it needs to actually do something, it gets added to a container and one or more systems now have a way
|
|
// to address the entity.
|
|
//
|
|
// systems that would be present for a character in a first person shooter for example
|
|
//
|
|
// --- game systems --- ones that the game would write and implement
|
|
//
|
|
// ( one or none of these three )
|
|
// player: recieves input messages from local input, forwards it to controller
|
|
// netplayer: recieves input messages from the network driver, forwards it to controller
|
|
// botplayer: generates control messages which drive bots and gameplay AI. forwards it to an controller
|
|
// - could use a behaviour tree under the hood
|
|
// arms: takes input actions and runs scripts to implement things that would be in front of the camera.
|
|
// - arms, weapons, etc...
|
|
// controller: converts raw player input to movement input for the character movement and other subsystems
|
|
// - eg. converts input axis to a vector movement
|
|
// - converts 'mouse_click_1' to a weapon fire call on the weapon component
|
|
// - also converts actions from the botplayer component which may send special components
|
|
// character: stores general gameplay relevant information about a given character.
|
|
// - gameplay stuff, updates values on the character_movement etc.. controls states, such as ragdolling
|
|
// character_movement: reconciles physics component data and movement coming in from controls
|
|
//
|
|
// --- engine systems --- ones that would come with the engine
|
|
// camera: Can be set as active. has a global state which tracks which camera is active
|
|
// physics: integration with the physics engine.
|
|
// animationGraph: animation graph drives skeletons inside the rendering_component
|
|
// renderScene: contains a list of subobjects which describe the visual-only scene for this character
|
|
// - renderScene will be the most complex one, lighter weight variants will include
|
|
// - staticMesh
|
|
// transform: final position of the character
|
|
// netAddress: acts as an endpoint to recieve network messages to this specific entity
|
|
//
|
|
// --- another thought experiment ----
|
|
// systems present in a mazing tower defense that is networked.
|
|
//
|
|
// game:
|
|
// mobs:
|
|
// mr/mazing:
|
|
// I think just this one system can handle the whole damn thing for enemies
|
|
// has a reference to the pathfinder entity. Which finds the best path for each enemy type to reach the
|
|
// main tower.
|
|
// mr/attributes:
|
|
// lightweight attributes which includes a buff/debuff system internally
|
|
// mr/pathfinder (singleton:):
|
|
// system which calls the pathfinding system to generate path segments for each of the segments of the map.
|
|
// can be queried based on position to tell the entity where to go next. includes an internal set of waypoints
|
|
//
|
|
// engine: systems that come with the engine
|
|
// p3/: peter's gameplay implementation toolbox.
|
|
// p3/grid_pathfinding: A* based pathfinding system, can create multiple grids and stitch them together.
|
|
//
|
|
// this is the kind of workflows I want to enable with ecs.
|
|
//
|
|
// - creating entities and associating that entity with any arbitrary number of systems
|
|
// - entities
|
|
//
|
|
// entities are globally unique ID handles. when you create an entity. you do so by basically asking
|
|
// the core system what set handle is free. this also adds that entity to the central registry
|
|
// which tracks stuff like if the object is completely free'd or not
|
|
//
|
|
// components which are a part of an entity are nothing more than an entry in another data structure called a container
|
|
// Any data structure can be used but the main data structures that are available for this purpose are all in
|
|
// p2/sparse-set.zig
|
|
//
|
|
// A few notes about this:
|
|
// Any data structure can be used as container storage it just needs a wrapper and implement
|
|
// the EcsContainerInterface found in sparse-set.zig
|
|
//
|
|
// current containers are:
|
|
//
|
|
// SparseMap (general purpose default, good enough for pretty much everything)
|
|
// SparseSet (only used for systsems where every entry gets iterated over every single frame)
|
|
// SparseMultiSet (AOS version of sparseSet Really specialized, only used for core engine systems)
|
|
|
|
pub fn createEntity() !Entity {
|
|
const rv = Entity{ .handle = try getRegistry().baseSet.createObject(.{}) };
|
|
// core.engine_log("entity created: {d}", .{rv.handle.index});
|
|
return rv;
|
|
}
|
|
|
|
pub fn destroyAllEntities() void {
|
|
const registry = getRegistry();
|
|
var elist = std.ArrayList(Entity){};
|
|
defer elist.deinit(registry.allocator);
|
|
|
|
core.engine_log("sparse set length {d}", .{registry.baseSet.dense.items.len});
|
|
for (registry.baseSet.dense.items) |dense| {
|
|
// TODO: shutdown allocator
|
|
elist.append(registry.allocator, Entity{ .handle = dense.sparseIndex }) catch {};
|
|
}
|
|
|
|
for (elist.items) |h| {
|
|
destroyEntity(h);
|
|
}
|
|
}
|
|
|
|
pub fn destroyEntity(e: Entity) void {
|
|
// core.engine_log("entity destroyed: {d}", .{e.handle.index});
|
|
const registry = getRegistry();
|
|
if (registry.baseSet.get(e.handle)) |entityEntry| {
|
|
for (entityEntry.containers.items) |ref| {
|
|
ref.vtable.destroyObject(ref.ptr, e.handle);
|
|
}
|
|
entityEntry.containers.deinit(registry.allocator);
|
|
registry.baseSet.destroyObject(e.handle);
|
|
} else {
|
|
// core.engine_log("UNABLE TO DESTROY ENTITY {d}", .{e.handle.index});
|
|
}
|
|
}
|
|
|
|
pub fn CreateEntity_Lua(state: lua.LuaState) i32 {
|
|
const ud = state.newZigUserdata(Entity) catch return 0;
|
|
ud.* = createEntity() catch {
|
|
std.debug.print("failed to create entity", .{});
|
|
return 0;
|
|
};
|
|
_ = state.getGlobal("__RegisterEntityProperty");
|
|
_ = state.pushValue(-2) catch return 0;
|
|
core.engine_log("Entity created: 0x{x}", .{ud.handle.index});
|
|
_ = state.pcallStack(1) catch {
|
|
std.debug.print("faield to exectute registration function", .{});
|
|
return 0;
|
|
};
|
|
return 1;
|
|
}
|
|
|
|
pub fn setup(allocator: std.mem.Allocator) !void {
|
|
_ = allocator;
|
|
// try ComponentRef.setupFormatBuffer(allocator);
|
|
_ = try core.createObject(EcsRegistry, .{ .can_tick = true, .isCore = true });
|
|
}
|
|
|
|
pub fn shutdown() void {
|
|
// ComponentRef.shutdownFormatBuffer();
|
|
}
|
|
|
|
pub const getRegistry = core.EngineObject(EcsRegistry).get;
|
|
|
|
pub fn registerEcsContainer(ref: EcsContainerRef, name: core.Name) !void {
|
|
try getRegistry().registerContainer(ref, name);
|
|
}
|
|
|
|
pub fn deregisterEcsContainer(ref: EcsContainerRef) void {
|
|
_ = ref;
|
|
@panic("not yet implemented");
|
|
}
|
|
|
|
pub fn createSystem(comptime System: type, allocator: std.mem.Allocator) !*System {
|
|
const system = try System.create(allocator);
|
|
const ref = p2.refFromPtr(EcsSystemInterface, system);
|
|
core.engine_log("ptr = {any}", .{ref.vtable.tick});
|
|
|
|
const registry = getRegistry();
|
|
try registry.systems.append(registry.allocator, ref);
|
|
if (ref.vtable.tick != null) {
|
|
try registry.tickableSystems.append(registry.allocator, ref);
|
|
}
|
|
|
|
return system;
|
|
}
|
|
|
|
// only thing this is meant to do is to provide a central place to construct and destroy objects
|
|
pub const EcsRegistry = struct {
|
|
allocator: std.mem.Allocator,
|
|
baseSet: BaseSet,
|
|
|
|
systems: std.ArrayListUnmanaged(EcsSystemRef) = .{},
|
|
tickableSystems: std.ArrayListUnmanaged(EcsSystemRef) = .{},
|
|
|
|
containers: std.ArrayListUnmanaged(EcsContainerRef) = .{},
|
|
containerNames: std.ArrayListUnmanaged(core.Name) = .{},
|
|
containersByName: std.AutoHashMapUnmanaged(u32, u32) = .{},
|
|
|
|
pub var NeonObjectTable = core.EngineObjectVTable.from(@This(), "core.EcsRegistry");
|
|
|
|
pub fn init(self: *@This(), allocator: std.mem.Allocator, first: bool) !void {
|
|
if (!first) {
|
|
return;
|
|
}
|
|
|
|
self.* = .{
|
|
.allocator = allocator,
|
|
.baseSet = BaseSet.init(allocator),
|
|
};
|
|
}
|
|
|
|
pub fn registerContainer(self: *@This(), ref: EcsContainerRef, _containerName: core.Name) !void {
|
|
var containerName = _containerName;
|
|
const newid = self.containers.items.len;
|
|
|
|
if (self.containersByName.contains(containerName.handle())) {
|
|
try core.assertf(false, "container already exists??", .{});
|
|
}
|
|
|
|
try self.containers.append(self.allocator, ref);
|
|
try self.containerNames.append(self.allocator, containerName);
|
|
try self.containersByName.put(self.allocator, containerName.handle(), @intCast(newid));
|
|
|
|
ref.vtable.onRegister(ref.ptr, @intCast(newid), .{
|
|
.ptr = @ptrCast(self),
|
|
.onHandleRemoved = onHandleRemoved,
|
|
.onHandleAdded = onHandleAdded,
|
|
});
|
|
}
|
|
|
|
pub fn onHandleRemoved(p: *anyopaque, containerID: u32, handle: core.ObjectHandle) void {
|
|
const self: *@This() = @ptrCast(@alignCast(p));
|
|
_ = containerID;
|
|
//core.engine_log("ECS:: object removed id={d} from container={s}({d}) 0x{x}", .{
|
|
// handle.index,
|
|
// self.containerNames.items[containerID].utf8(),
|
|
// containerID,
|
|
// @intFromPtr(self.containers.items[containerID].ptr),
|
|
//});
|
|
|
|
self.baseSet.get(handle).?.containersCount -= 1;
|
|
}
|
|
|
|
pub fn onHandleAdded(p: *anyopaque, containerID: u32, handle: core.ObjectHandle) void {
|
|
const self: *@This() = @ptrCast(@alignCast(p));
|
|
_ = containerID;
|
|
// core.engine_log("ECS:: object added id=0x{x} from container={s}({d}) 0x{x}", .{
|
|
// handle.index,
|
|
// self.containerNames.items[containerID].utf8(),
|
|
// containerID,
|
|
// @intFromPtr(self.containers.items[containerID].ptr),
|
|
// });
|
|
//
|
|
if (self.baseSet.get(handle)) |obj| {
|
|
obj.containersCount += 1;
|
|
} else {
|
|
_ = self.baseSet.createWithHandle(handle, .{ .containersCount = 1 }) catch unreachable;
|
|
}
|
|
}
|
|
|
|
pub fn tick(self: *@This(), deltaTime: f64) void {
|
|
if (core.getEngine().isShuttingDown())
|
|
return;
|
|
for (self.tickableSystems.items) |ref| {
|
|
ref.vtable.tick.?(ref.ptr, deltaTime);
|
|
}
|
|
}
|
|
|
|
pub fn deinit(self: *@This()) void {
|
|
// this should never work... wtf?
|
|
// for (self.containers.items) |ref| {
|
|
// ref.vtable.evictFromRegistry(ref.ptr);
|
|
// }
|
|
|
|
for (self.systems.items) |ref| {
|
|
ref.vtable.destroy(ref.ptr);
|
|
}
|
|
|
|
for (self.baseSet.dense.items) |*entry| {
|
|
entry.value.containers.deinit(self.allocator);
|
|
}
|
|
|
|
self.systems.deinit(self.allocator);
|
|
self.tickableSystems.deinit(self.allocator);
|
|
self.baseSet.deinit();
|
|
self.containers.deinit(self.allocator);
|
|
self.containerNames.deinit(self.allocator);
|
|
self.containersByName.deinit(self.allocator);
|
|
}
|
|
};
|
|
|
|
pub fn defineComponentList(comptime ComponentList: type, allocator: std.mem.Allocator) !void {
|
|
const typeInfo = @typeInfo(ComponentList).@"struct";
|
|
|
|
inline for (typeInfo.decls) |decl| {
|
|
core.engine_logs("defining component" ++ decl.name);
|
|
try defineComponent(@field(ComponentList, decl.name), allocator);
|
|
}
|
|
}
|
|
|
|
pub fn undefineComponentList(comptime ComponentList: type) void {
|
|
const typeInfo = @typeInfo(ComponentList).@"struct";
|
|
inline for (typeInfo.decls) |decl| {
|
|
undefineComponent(@field(ComponentList, decl.name));
|
|
}
|
|
}
|
|
|
|
// this doesn't do any behaviour patching right now,
|
|
// only BaseContainer pointers
|
|
pub fn patchComponentList(comptime ComponentList: type) void {
|
|
const typeInfo = @typeInfo(ComponentList).@"struct";
|
|
if (core.getEngineObject(EcsRegistry)) |registry| {
|
|
inline for (typeInfo.decls) |decl| {
|
|
const T = @field(ComponentList, decl.name);
|
|
var name = core.MakeName(T.ComponentName);
|
|
|
|
if (registry.containersByName.get(name.handle())) |offset| {
|
|
core.engine_log("Patching component {s}", .{name.utf8()});
|
|
const ref = registry.containers.items[offset];
|
|
T.BaseContainer = @ptrCast(@alignCast(ref.ptr));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn defineComponent(comptime Component: type, allocator: std.mem.Allocator) !void {
|
|
const ContainerType = @TypeOf(Component.BaseContainer.*);
|
|
Component.BaseContainer = try ContainerType.create(allocator);
|
|
|
|
core.engine_log("Component container created " ++ @typeName(Component) ++ " @{x} component size = {d} containerType = {s}", .{ @intFromPtr(Component.BaseContainer), @sizeOf(Component), @TypeOf(Component.BaseContainer.*).EcsContainerInterfaceVTable.containerTypeName });
|
|
const container = makeEcsContainerRef(Component.BaseContainer);
|
|
try registerEcsContainer(container, core.MakeName(Component.ComponentName));
|
|
|
|
// try script.registerComponent(Component, container);
|
|
}
|
|
|
|
pub fn undefineComponent(comptime Component: type) void {
|
|
core.engine_logs("undefining component " ++ @typeName(Component));
|
|
Component.BaseContainer.destroy();
|
|
}
|
|
|
|
pub const Entity = struct {
|
|
handle: core.ObjectHandle,
|
|
|
|
pub const PodDataTable: pod.DataTable = .{
|
|
.name = "Entity",
|
|
.newFuncOverride = lua.CWrap(CreateEntity_Lua),
|
|
.luaDirectFuncs = &.{
|
|
.{ .name = "addComponent", .func = "luaAddComponent" },
|
|
//.{ .name = "get", .func = "luaAddComponent" },
|
|
},
|
|
};
|
|
|
|
pub fn destroy(self: @This()) void {
|
|
//if (core.getEngineObject(EcsRegistry) != null) {
|
|
destroyEntity(self);
|
|
// }
|
|
}
|
|
|
|
pub fn fromHandle(handle: core.ObjectHandle) @This() {
|
|
return .{ .handle = handle };
|
|
}
|
|
|
|
pub fn addComponent(self: @This(), comptime Component: type) ?*Component {
|
|
// core.engine_log("adding component: {d} {s}", .{ self.handle.index, @typeName(Component) });
|
|
const rv = Component.BaseContainer.createWithHandleECS(self.handle);
|
|
|
|
const list = &getRegistry().baseSet.get(self.handle).?.containers;
|
|
const allocator = getRegistry().allocator;
|
|
list.append(allocator, getTypeContainer(Component)) catch return null;
|
|
|
|
// if (@hasDecl(Component, "init")) {
|
|
// rv.init(self.handle);
|
|
// }
|
|
|
|
return rv;
|
|
}
|
|
|
|
pub fn fetch(self: @This(), comptime Component: type) ?*Component {
|
|
if (@TypeOf(Component.BaseContainer.*).StableReferences) {
|
|
@compileError(@typeName(Component) ++ " states that it's references are stable, use get() instead of fetch()");
|
|
}
|
|
const rv = Component.BaseContainer.get(self.handle);
|
|
return if (rv == null) null else @ptrCast(@alignCast(rv.?));
|
|
}
|
|
|
|
// acquire a reference to an entity's component if it exists
|
|
pub fn get(self: @This(), comptime Component: type) ?*Component {
|
|
if (!@TypeOf(Component.BaseContainer.*).StableReferences) {
|
|
@compileError(@typeName(Component) ++ " doesn't state that it's references are stable, use fetch() instead of get()");
|
|
}
|
|
const rv = Component.BaseContainer.get(self.handle);
|
|
return if (rv == null) null else @ptrCast(@alignCast(rv.?));
|
|
}
|
|
|
|
pub fn removeComponent(self: @This(), comptime Component: type) void {
|
|
Component.BaseContainer.remove(self.handle);
|
|
}
|
|
|
|
pub fn luaAddComponent(state: lua.LuaState) i32 {
|
|
const argc = state.getTop();
|
|
|
|
if (argc != 2) {
|
|
core.engine_log("Add Component Error, expected 2 arguments got {d}", .{argc});
|
|
return 0;
|
|
}
|
|
|
|
// if (state.toUserdata(@This(), 1)) |self| {
|
|
// if (state.toUserdata(ComponentRegistration, 2)) |componentRegistration| {
|
|
// // core.engine_log("component Registration: name: {s}", .{componentRegistration.name});
|
|
|
|
// // create the zig version of the component
|
|
// const comp = componentRegistration.createComponent(self.handle);
|
|
|
|
// // call luaNew on ComponentReferenceType
|
|
// // create the lua binding for the component
|
|
// // this pushes one onto the stack
|
|
// componentRegistration.luaNew(state, self.handle, comp);
|
|
// }
|
|
// }
|
|
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
pub const EcsContainerInterface = p2.EcsContainerInterface;
|
|
pub const EcsContainerRef = p2.Reference(EcsContainerInterface);
|
|
pub fn makeEcsContainerRef(ptr: anytype) EcsContainerRef {
|
|
return p2.refFromPtr(EcsContainerInterface, ptr);
|
|
}
|
|
|
|
pub fn getTypeContainer(comptime T: type) EcsContainerRef {
|
|
return makeEcsContainerRef(T.BaseContainer);
|
|
}
|
|
|
|
pub const EcsEntry = struct {
|
|
containersCount: u32 = 0,
|
|
containers: std.ArrayListUnmanaged(EcsContainerRef) = .{},
|
|
};
|
|
|
|
pub const BaseSet = p2.SparseSet(EcsEntry);
|
|
|
|
pub const EcsSystemRef = p2.Reference(EcsSystemInterface);
|
|
pub const EcsSystemInterface = p2.MakeInterface("EcsSystemVTable", struct {
|
|
create: *const fn (std.mem.Allocator) core.EngineDataEventError!*anyopaque,
|
|
destroy: *const fn (*anyopaque) void,
|
|
tick: ?*const fn (*anyopaque, f64) void = null,
|
|
|
|
pub fn Implement(comptime TargetType: type) @This() {
|
|
const Wrap = struct {
|
|
pub fn create(allocator: std.mem.Allocator) core.EngineDataEventError!*anyopaque {
|
|
const new = try TargetType.create(allocator);
|
|
return new;
|
|
}
|
|
|
|
pub fn destroy(p: *anyopaque) void {
|
|
const ptr: *TargetType = @ptrCast(@alignCast(p));
|
|
ptr.destroy();
|
|
}
|
|
|
|
pub fn tick(p: *anyopaque, dt: f64) void {
|
|
const ptr: *TargetType = @ptrCast(@alignCast(p));
|
|
ptr.tick(dt);
|
|
}
|
|
};
|
|
return .{
|
|
.destroy = Wrap.destroy,
|
|
.create = Wrap.create,
|
|
.tick = if (@hasDecl(TargetType, "tick")) Wrap.tick else null,
|
|
};
|
|
}
|
|
});
|
|
|
|
// const ComponentRegistration = @import("script/ComponentRegistration.zig");
|
|
// const ComponentRef = @import("script/ComponentRef.zig");
|
|
// const script = @import("script.zig");
|
|
const std = @import("std");
|
|
const p2 = @import("p2");
|
|
const core = @import("core.zig");
|
|
const lua = @import("lua");
|
|
const pod = lua.pod;
|
|
const scene = @import("scene.zig");
|