diff --git a/assets/scenes/test.json b/assets/scenes/test.json index 39d70760..a48285d2 100644 --- a/assets/scenes/test.json +++ b/assets/scenes/test.json @@ -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": [ diff --git a/src/dusk/entity/CMakeLists.txt b/src/dusk/entity/CMakeLists.txt index e886dae7..8e887aca 100644 --- a/src/dusk/entity/CMakeLists.txt +++ b/src/dusk/entity/CMakeLists.txt @@ -9,6 +9,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} entity.c entitymanager.c component.c + entityprefab.c ) # Subdirs diff --git a/src/dusk/entity/entity.c b/src/dusk/entity/entity.c index 52329d30..f922d80a 100644 --- a/src/dusk/entity/entity.c +++ b/src/dusk/entity/entity.c @@ -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(); diff --git a/src/dusk/entity/entity.h b/src/dusk/entity/entity.h index 9449d920..523b7d26 100644 --- a/src/dusk/entity/entity.h +++ b/src/dusk/entity/entity.h @@ -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/.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( diff --git a/src/dusk/entity/entityprefab.c b/src/dusk/entity/entityprefab.c new file mode 100644 index 00000000..c4eb1957 --- /dev/null +++ b/src/dusk/entity/entityprefab.c @@ -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(); +} diff --git a/src/dusk/entity/entityprefab.h b/src/dusk/entity/entityprefab.h new file mode 100644 index 00000000..77b2e42d --- /dev/null +++ b/src/dusk/entity/entityprefab.h @@ -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/.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 +); diff --git a/src/dusk/entity/entityprefablist.h b/src/dusk/entity/entityprefablist.h new file mode 100644 index 00000000..da394fe5 --- /dev/null +++ b/src/dusk/entity/entityprefablist.h @@ -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" diff --git a/src/dusk/game/game.h b/src/dusk/game/game.h index 2b7041ff..1da43eae 100644 --- a/src/dusk/game/game.h +++ b/src/dusk/game/game.h @@ -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 -); diff --git a/src/duskrpg/entity/CMakeLists.txt b/src/duskrpg/entity/CMakeLists.txt index ea2a04bc..61e8dfd6 100644 --- a/src/duskrpg/entity/CMakeLists.txt +++ b/src/duskrpg/entity/CMakeLists.txt @@ -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) diff --git a/src/duskrpg/entity/gameprefablist.h b/src/duskrpg/entity/gameprefablist.h new file mode 100644 index 00000000..883870d5 --- /dev/null +++ b/src/duskrpg/entity/gameprefablist.h @@ -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) diff --git a/src/duskrpg/entity/prefab/CMakeLists.txt b/src/duskrpg/entity/prefab/CMakeLists.txt new file mode 100644 index 00000000..f06b795f --- /dev/null +++ b/src/duskrpg/entity/prefab/CMakeLists.txt @@ -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 +) diff --git a/src/duskrpg/entity/prefab/entityprefabplayer.c b/src/duskrpg/entity/prefab/entityprefabplayer.c new file mode 100644 index 00000000..e83323f3 --- /dev/null +++ b/src/duskrpg/entity/prefab/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(); +} diff --git a/src/duskrpg/entity/prefab/entityprefabplayer.h b/src/duskrpg/entity/prefab/entityprefabplayer.h new file mode 100644 index 00000000..9199a55f --- /dev/null +++ b/src/duskrpg/entity/prefab/entityprefabplayer.h @@ -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 +); diff --git a/src/duskrpg/game/game.c b/src/duskrpg/game/game.c index 41467eeb..baef87b6 100644 --- a/src/duskrpg/game/game.c +++ b/src/duskrpg/game/game.c @@ -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); -}