Add prefab support

This commit is contained in:
2026-07-20 14:44:14 -05:00
parent b9f06fef7a
commit 38c2f6b6f9
14 changed files with 294 additions and 69 deletions
+1 -13
View File
@@ -23,19 +23,7 @@
},
{
"name": "player",
"components": [
{ "type": "POSITION", "x": 0, "y": 5, "z": 0 },
{
"type": "RENDERABLE",
"renderType": "SHADER_MATERIAL",
"priority": 0,
"shaderType": "UNLIT",
"color": { "r": 255, "g": 80, "b": 80, "a": 255 },
"displayState": { "cull": false, "depthTest": true, "blend": false }
},
{ "type": "PLAYER", "moveSpeed": 4, "jumpImpulse": 6 },
{ "type": "PHYSICS", "bodyType": "DYNAMIC", "collideMask": 3 }
]
"extend": "PLAYER"
},
{
"components": [
+1
View File
@@ -9,6 +9,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
entity.c
entitymanager.c
component.c
entityprefab.c
)
# Subdirs
+15
View File
@@ -7,6 +7,7 @@
#include "entitymanager.h"
#include "component/display/entityposition.h"
#include "entityprefab.h"
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
@@ -261,6 +262,20 @@ errorret_t entityDeserialize(
yyjson_val *nameVal = yyjson_obj_get(json, "name");
if(nameVal) entitySetName(mgr, entityId, yyjson_get_str(nameVal));
yyjson_val *extendVal = yyjson_obj_get(json, "extend");
if(extendVal) {
errorChain(entityPrefabResolveAndApply(
mgr, entityId, yyjson_get_str(extendVal)
));
}
yyjson_val *prefabVal = yyjson_obj_get(json, "prefab");
if(prefabVal) {
errorChain(entityPrefabResolveAndApply(
mgr, entityId, yyjson_get_str(prefabVal)
));
}
yyjson_val *components = yyjson_obj_get(json, "components");
if(!components) errorOk();
+23 -9
View File
@@ -226,18 +226,32 @@ errorret_t entitySerialize(
);
/**
* Deserializes an entity's optional "name" and its components from the
* given JSON object's "components" array. For each component entry,
* adds a component of the type named by its "type" field (matched
* against COMPONENT_DEFINITIONS[].enumName) and hands the entry to that
* component type's deserialize callback (see componentDeserialize()).
* No-op for "components" if json has no such array.
* Deserializes an entity's optional "name", "extend"/"prefab", and its
* components from the given JSON object's "components" array.
*
* If present, "extend" and then "prefab" are each resolved and applied
* as a prefab (see entityPrefabResolveAndApply()) before "components" is
* processed -- both keys do the same thing (resolve a named prefab,
* C-coded first then a "prefabs/<name>.json" asset, and apply it to this
* entity); "extend" is the conventional key when this JSON is itself a
* prefab definition inheriting from a parent, "prefab" is the
* conventional key when a plain entity wants to be defined by a prefab.
* A prefab-added component still counts as already added, so listing the
* same component type again in "components" throws (see
* entityAddComponent()).
*
* For each "components" entry, adds a component of the type named by its
* "type" field (matched against COMPONENT_DEFINITIONS[].enumName) and
* hands the entry to that component type's deserialize callback (see
* componentDeserialize()). No-op for "components" if json has no such
* array.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The ID of the entity to deserialize into. Must not
* already have any of the components named in json.
* @param json The JSON object to read the entity's "name"/"components"
* keys from.
* already have any of the components named in json (including any a
* resolved "extend"/"prefab" prefab already added).
* @param json The JSON object to read the entity's "name"/"extend"/
* "prefab"/"components" keys from.
* @return Error state.
*/
errorret_t entityDeserialize(
+72
View File
@@ -0,0 +1,72 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityprefab.h"
#include "entity.h"
#include "asset/asset.h"
#include "util/string.h"
#include "assert/assert.h"
#include "yyjson.h"
// Pulls in every prefab's real header (entityprefablist.h's one-time
// #include lines) exactly once -- each header's #pragma once then makes
// the array-building pass below a no-op for those lines, leaving only
// bare X(...) invocations to expand.
#define X(enumName, extends, apply) // do nothing
#include "entityprefablist.h"
#undef X
entityprefab_t ENTITY_PREFABS[] = {
#define X(enm, extendsName, applyFn) \
{ .name = #enm, .extends = extendsName, .apply = applyFn },
#include "entityprefablist.h"
#undef X
// Sentinel: name[0] == '\0' marks the end for callers iterating without
// a separate count.
{ 0 }
};
errorret_t entityPrefabInit(
entitymanager_t *mgr,
const entityid_t entityId,
const entityprefab_t prefab
) {
assertNotNull(prefab.apply, "Prefab apply function cannot be null");
if(prefab.extends[0] != '\0') {
errorChain(entityPrefabResolveAndApply(mgr, entityId, prefab.extends));
}
errorChain(prefab.apply(mgr, entityId));
errorOk();
}
errorret_t entityPrefabResolveAndApply(
entitymanager_t *mgr,
const entityid_t entityId,
const char_t *name
) {
for(size_t i = 0; ENTITY_PREFABS[i].name[0] != '\0'; i++) {
if(!stringEquals(ENTITY_PREFABS[i].name, name)) continue;
errorChain(entityPrefabInit(mgr, entityId, ENTITY_PREFABS[i]));
errorOk();
}
char_t assetPath[ASSET_FILE_NAME_MAX];
stringFormat(assetPath, ASSET_FILE_NAME_MAX - 1, "prefabs/%s.json", name);
assetentry_t *entry = assetLock(assetPath, ASSET_LOADER_TYPE_JSON, NULL);
errorret_t ret = assetRequireLoaded(entry);
if(errorIsOk(ret)) {
ret = entityDeserialize(
mgr, entityId, yyjson_doc_get_root(entry->data.json)
);
}
assetUnlockEntry(entry);
errorChain(ret);
errorOk();
}
+75
View File
@@ -0,0 +1,75 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entitybase.h"
#include "error/error.h"
#define ENTITY_PREFAB_NAME_MAX 32
/**
* Applies a prefab to an entity, e.g. adding and configuring whatever
* components make up the prefab.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The ID of the entity to apply the prefab to.
* @return Error state.
*/
typedef errorret_t (*entityprefabapply_t)(
entitymanager_t *mgr,
const entityid_t entityId
);
typedef struct {
char_t name[ENTITY_PREFAB_NAME_MAX];
char_t extends[ENTITY_PREFAB_NAME_MAX];
entityprefabapply_t apply;
} entityprefab_t;
/**
* The C-coded prefab registry, built from entityprefablist.h. Terminated
* by a sentinel entry whose name[0] is '\0' -- iterate with a
* `for(...; ENTITY_PREFABS[i].name[0] != '\0'; ...)` style loop rather
* than a fixed count.
*/
extern entityprefab_t ENTITY_PREFABS[];
/**
* Initializes an entity from a prefab, running its apply function. If the
* prefab declares a parent (prefab.extends is non-empty), that parent is
* resolved and applied first (see entityPrefabResolveAndApply()), so this
* prefab's own apply function runs on top of/after it.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The ID of the entity to apply the prefab to.
* @param prefab The prefab to apply.
* @return Error state.
*/
errorret_t entityPrefabInit(
entitymanager_t *mgr,
const entityid_t entityId,
const entityprefab_t prefab
);
/**
* Resolves a prefab by name and applies it to the given entity. First
* searches the C-coded ENTITY_PREFABS[] registry (see
* entityprefablist.h) for a matching name; if none match, falls back to
* loading "prefabs/<name>.json" as a JSON asset and applying it the same
* way an entity's own JSON definition is applied (see entityDeserialize()
* and its "prefab"/"extend" keys).
*
* @param mgr The entity manager that owns the entity.
* @param entityId The ID of the entity to apply the prefab to.
* @param name The prefab name to resolve.
* @return Error state.
*/
errorret_t entityPrefabResolveAndApply(
entitymanager_t *mgr,
const entityid_t entityId,
const char_t *name
);
+13
View File
@@ -0,0 +1,13 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
// Name (Uppercase)
// Extends (name of another prefab to apply first, or "" for none)
// Apply function
// Game-specific prefabs
#include "entity/gameprefablist.h"
-22
View File
@@ -7,7 +7,6 @@
#pragma once
#include "error/error.h"
#include "entity/entitybase.h"
/**
* Initializes the game. Called once, after the engine and all of its
@@ -31,24 +30,3 @@ errorret_t gameUpdate(void);
* @return An error code indicating success or failure.
*/
errorret_t gameDispose(void);
/**
* Test-only TRIGGER onEnter callback, subscribed by gameInit() to the
* "testArea" entity in the test scene (see assets/scenes/test.json).
* Prints a debug message identifying whichever entity just entered.
*
* @param mgr The entity manager that owns both entities.
* @param triggerEntityId The entity ID of the "testArea" trigger volume.
* @param triggerComponentId The TRIGGER component ID.
* @param otherEntityId The entity ID that entered the trigger volume.
* @param otherComponentId The other entity's POSITION component ID.
* @param user Unused.
*/
void gameTestAreaOnEnter(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId,
void *user
);
+2 -1
View File
@@ -3,6 +3,7 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Header-only: gamecomponentlist.h is an X-macro list, no implementation.
# gameprefablist.h is an X-macro list, no implementation of its own.
add_subdirectory(component)
add_subdirectory(prefab)
+16
View File
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
// Game-specific prefabs, appended after the engine's inbuilt prefabs in
// entity/entityprefablist.h.
#include "entity/prefab/entityprefabplayer.h"
// Name (Uppercase)
// Extends (name of another prefab to apply first, or "" for none)
// Apply function
X(PLAYER, "", entityPrefabPlayerApply)
+9
View File
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
entityprefabplayer.c
)
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityprefabplayer.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityposition.h"
#include "entity/component/display/entityrenderable.h"
#include "entity/component/physics/entityphysics.h"
#include "display/color.h"
errorret_t entityPrefabPlayerApply(
entitymanager_t *mgr,
const entityid_t entityId
) {
componentid_t posComp = entityAddComponent(
mgr, entityId, COMPONENT_TYPE_POSITION
);
vec3 startPos = { 0.0f, 5.0f, 0.0f };
entityPositionSetLocalPosition(mgr, entityId, posComp, startPos);
componentid_t renderComp = entityAddComponent(
mgr, entityId, COMPONENT_TYPE_RENDERABLE
);
entityRenderableSetColor(
mgr, entityId, renderComp, color4b(255, 80, 80, 255)
);
entityAddComponent(mgr, entityId, COMPONENT_TYPE_PLAYER);
componentid_t physComp = entityAddComponent(
mgr, entityId, COMPONENT_TYPE_PHYSICS
);
entityPhysicsSetBodyType(mgr, entityId, physComp, PHYSICS_BODY_DYNAMIC);
entityPhysicsSetCollideMask(mgr, entityId, physComp, 0x3);
errorOk();
}
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
#include "error/error.h"
/**
* Applies the PLAYER prefab: a POSITION 5 units up, a red
* SHADER_MATERIAL RENDERABLE, a PLAYER component, and a DYNAMIC PHYSICS
* body tagged with collideMask 0x3 (world + player layers). C-coded
* equivalent of the hand-authored "player" entity in
* assets/scenes/test.json.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The ID of the entity to apply the prefab to.
* @return Error state.
*/
errorret_t entityPrefabPlayerApply(
entitymanager_t *mgr,
const entityid_t entityId
);
-24
View File
@@ -34,19 +34,6 @@ errorret_t gameInit(void) {
assetUnlockEntry(sceneEntry);
errorChain(ret);
// Test-only: print a debug message whenever an entity enters "testArea"
// (see assets/scenes/test.json), exercising the TRIGGER component.
entitymanager_t *mgr = sceneGetEntities(testSceneId);
entityid_t testAreaEntity = entityFindByName(mgr, "testArea");
if(testAreaEntity != ENTITY_ID_INVALID) {
componentid_t testAreaTrig = entityGetComponent(
mgr, testAreaEntity, COMPONENT_TYPE_TRIGGER
);
entityTriggerOnEnterAdd(
mgr, testAreaEntity, testAreaTrig, gameTestAreaOnEnter, NULL
);
}
sceneSetActive(testSceneId);
errorOk();
}
@@ -58,14 +45,3 @@ errorret_t gameUpdate(void) {
errorret_t gameDispose(void) {
errorOk();
}
void gameTestAreaOnEnter(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId,
void *user
) {
logDebug("testArea: entity %d entered", otherEntityId);
}