From 4688358d6a8ac2203ad105db304ff04b9bbe4641 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Thu, 21 Aug 2025 09:52:15 -0500 Subject: [PATCH] Scene tree --- src/CMakeLists.txt | 2 + src/display/CMakeLists.txt | 2 + src/ecs/CMakeLists.txt | 10 ++++ src/ecs/ecs.c | 59 +++++++++++++++++++ src/ecs/ecs.h | 51 ++++++++++++++++ src/engine/engine.c | 4 ++ src/scene/CMakeLists.txt | 10 ++++ src/scene/scene.h | 15 ----- src/scene/scenetree.c | 117 +++++++++++++++++++++++++++++++++++++ src/scene/scenetree.h | 91 +++++++++++++++++++++++++++++ 10 files changed, 346 insertions(+), 15 deletions(-) create mode 100644 src/ecs/CMakeLists.txt create mode 100644 src/ecs/ecs.c create mode 100644 src/ecs/ecs.h create mode 100644 src/scene/CMakeLists.txt delete mode 100644 src/scene/scene.h create mode 100644 src/scene/scenetree.c create mode 100644 src/scene/scenetree.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7b557db..8bb17b3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,10 +25,12 @@ target_sources(${DUSK_TARGET_NAME} add_subdirectory(assert) add_subdirectory(console) add_subdirectory(display) +add_subdirectory(ecs) add_subdirectory(engine) add_subdirectory(error) add_subdirectory(input) add_subdirectory(locale) +add_subdirectory(scene) add_subdirectory(thread) add_subdirectory(time) add_subdirectory(util) \ No newline at end of file diff --git a/src/display/CMakeLists.txt b/src/display/CMakeLists.txt index de672e7..905ee41 100644 --- a/src/display/CMakeLists.txt +++ b/src/display/CMakeLists.txt @@ -13,5 +13,7 @@ if(DUSK_TARGET_SYSTEM STREQUAL "linux") target_compile_definitions(${DUSK_TARGET_NAME} PRIVATE DUSK_DISPLAY_SDL2=1 + DISPLAY_WINDOW_WIDTH_DEFAULT=960 + DISPLAY_WINDOW_HEIGHT_DEFAULT=720 ) endif() \ No newline at end of file diff --git a/src/ecs/CMakeLists.txt b/src/ecs/CMakeLists.txt new file mode 100644 index 0000000..72e981c --- /dev/null +++ b/src/ecs/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_TARGET_NAME} + PRIVATE + ecs.c +) \ No newline at end of file diff --git a/src/ecs/ecs.c b/src/ecs/ecs.c new file mode 100644 index 0000000..3028305 --- /dev/null +++ b/src/ecs/ecs.c @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "ecs.h" +#include "util/memory.h" +#include "assert/assert.h" + +ecs_t ECS; + +void ecsInit() { + memoryZero(&ECS, sizeof(ecs_t)); + + // Fill ECS ids + for(uint32_t i = 0; i < ECS_ENTITY_COUNT_MAX; i++) { + ECS.entities[i].id = i; + } + + // Fill the available array. + for(uint32_t i = 0; i < ECS_ENTITY_COUNT_MAX; i++) { + ECS.available[i] = &ECS.entities[i]; + } + ECS.availableCount = ECS_ENTITY_COUNT_MAX; + + // Reserve root entity + ECS.root = ecsEntityCreate(); +} + +ecsentityid_t ecsEntityCreate() { + assertTrue(ECS.availableCount > 0, "No available entities to create"); + + // Pop off the last available entity. + ecsentity_t *entity = ECS.available[--ECS.availableCount]; + assertTrue((entity->flags & ECS_ENTITY_FLAG_USED) == 0, "Entity is used."); + assertTrue(entity->id >= 0, "Entity is invalid."); + assertTrue(entity->id < ECS_ENTITY_COUNT_MAX, "Entity ID out of bounds"); + + entity->flags |= ECS_ENTITY_FLAG_USED; + + return entity->id; +} + +void ecsEntityDispose(const ecsentityid_t id) { + assertTrue(id < ECS_ENTITY_COUNT_MAX, "Invalid entity ID"); + + ecsentity_t *entity = &ECS.entities[id]; + assertTrue(entity->id >= 0, "Entity is invalid."); + assertTrue((entity->flags & ECS_ENTITY_FLAG_USED) != 0, "Entity is not used."); + + // Mark the entity as available. + ECS.available[ECS.availableCount++] = entity; + assertTrue( + ECS.availableCount <= ECS_ENTITY_COUNT_MAX, + "Available count exceeded maximum limit" + ); +} \ No newline at end of file diff --git a/src/ecs/ecs.h b/src/ecs/ecs.h new file mode 100644 index 0000000..76edcbc --- /dev/null +++ b/src/ecs/ecs.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef int_fast16_t ecsentityid_t; + +#define ECS_ENTITY_COUNT_MAX 2048 +#define ECS_ENTITY_FLAG_USED (1 << 0) + +typedef struct ecsentity_s { + ecsentityid_t id; + uint8_t flags; +} ecsentity_t; + +typedef struct ecs_s { + ecsentity_t entities[ECS_ENTITY_COUNT_MAX]; + + ecsentity_t *available[ECS_ENTITY_COUNT_MAX]; + uint32_t availableCount; + + ecsentityid_t root; +} ecs_t; + +extern ecs_t ECS; + +/** + * Initialize the given ECS system. + */ +void ecsInit(); + +/** + * Create a new entity in the ECS. This locks an id and gives it to the caller, + * who will be responsible for managing the entity's lifecycle. + * + * @return The ID of the newly created entity. + */ +ecsentityid_t ecsEntityCreate(); + +/** + * Disposes an entity in the ECS. This will free the entity's ID and make it + * available for reuse. + * + * @param id The ID of the entity to destroy. + */ +void ecsEntityDispose(const ecsentityid_t id); \ No newline at end of file diff --git a/src/engine/engine.c b/src/engine/engine.c index c56b110..2c2741b 100644 --- a/src/engine/engine.c +++ b/src/engine/engine.c @@ -10,6 +10,8 @@ #include "time/time.h" #include "console/console.h" #include "display/display.h" +#include "ecs/ecs.h" +#include "scene/scenetree.h" engine_t ENGINE; @@ -20,6 +22,8 @@ errorret_t engineInit(void) { // Init systems. Order is important. timeInit(); consoleInit(); + ecsInit(); + sceneTreeInit(); errorChain(displayInit()); errorOk(); diff --git a/src/scene/CMakeLists.txt b/src/scene/CMakeLists.txt new file mode 100644 index 0000000..b49a86a --- /dev/null +++ b/src/scene/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_TARGET_NAME} + PRIVATE + scenetree.c +) \ No newline at end of file diff --git a/src/scene/scene.h b/src/scene/scene.h deleted file mode 100644 index e2ebbbb..0000000 --- a/src/scene/scene.h +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -typedef struct { - int padding; -} scene_t; - -extern scene_t *SCENE; \ No newline at end of file diff --git a/src/scene/scenetree.c b/src/scene/scenetree.c new file mode 100644 index 0000000..a73ca18 --- /dev/null +++ b/src/scene/scenetree.c @@ -0,0 +1,117 @@ +// Copyright (c) 2025 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +#include "scenetree.h" +#include "util/memory.h" +#include "assert/assert.h" + +scenetree_t SCENE_TREE; + +void sceneTreeInit(void) { + memoryZero(&SCENE_TREE, sizeof(scenetree_t)); + + // Default all items to have no parent + for (ecsentityid_t i = 0; i < ECS_ENTITY_COUNT_MAX; i++) { + SCENE_TREE.items[i].parent = -1; + } +} + +ecsentityid_t sceneTreeParentGet(const ecsentityid_t child) { + assertTrue(child >= 0 && child < ECS_ENTITY_COUNT_MAX, "Invalid child ID"); + return SCENE_TREE.items[child].parent; +} + +uint8_t sceneTreeChildGetAll(const ecsentityid_t id, ecsentityid_t *children) { + assertTrue(id >= 0 && id < ECS_ENTITY_COUNT_MAX, "Invalid parent ID"); + + const scenetreenode_t *node = &SCENE_TREE.items[id]; + if(children == NULL) return node->childCount; + + return node->childCount; +} + +void sceneTreeChildAdd(const ecsentityid_t parent, const ecsentityid_t child) { + assertTrue(parent >= 0 && parent < ECS_ENTITY_COUNT_MAX, "Invalid parent ID"); + assertTrue(child >= 0 && child < ECS_ENTITY_COUNT_MAX, "Invalid child ID"); + assertTrue(parent != child, "Child cannot be a child of itself."); + assertFalse( + sceneTreeChildInTree(parent, child), + "Child is already in the parent's tree" + ); + assertFalse( + sceneTreeChildInTree(child, parent), + "Child cannot be a deep child of itself." + ); + + scenetreenode_t *parentNode = &SCENE_TREE.items[parent]; + assertTrue(parentNode->childCount < SCENE_ITEM_CHILD_MAX, "Parent full."); + + // Add child to parent's children array + parentNode->children[parentNode->childCount] = child; + parentNode->childCount++; + + // Set child's parent + SCENE_TREE.items[child].parent = parent; +} + +void sceneTreeChildRemove( + const ecsentityid_t parent, + const ecsentityid_t child +) { + assertTrue(parent >= 0 && parent < ECS_ENTITY_COUNT_MAX, "Invalid parent ID"); + assertTrue(child >= 0 && child < ECS_ENTITY_COUNT_MAX, "Invalid child ID"); + assertTrue(parent != child, "Child cannot be a child of itself."); + + scenetreenode_t *childNode = &SCENE_TREE.items[child]; + scenetreenode_t *parentNode = &SCENE_TREE.items[parent]; + assertTrue(childNode->parent == parent, "Child does not belong to parent"); + assertTrue(parentNode->childCount > 0, "Parent has no children"); + + // Remove child from parent's children array + for (uint8_t i = 0; i < parentNode->childCount; i++) { + if (parentNode->children[i] == child) { + // Shift remaining children down + for (uint8_t j = i; j < parentNode->childCount - 1; j++) { + parentNode->children[j] = parentNode->children[j + 1]; + } + parentNode->childCount--; + break; + } + } + + // Clear child's parent + childNode->parent = -1; +} + +void sceneTreeChildRemoveAll(const ecsentityid_t parent) { + assertTrue(parent >= 0 && parent < ECS_ENTITY_COUNT_MAX, "Invalid parent ID"); + + scenetreenode_t *parentNode = &SCENE_TREE.items[parent]; + for(uint8_t i = 0; i < parentNode->childCount; i++) { + ecsentityid_t child = parentNode->children[i]; + SCENE_TREE.items[child].parent = -1; + } + + // Reset child count + parentNode->childCount = 0; +} + +bool_t sceneTreeChildInTree( + const ecsentityid_t parent, + const ecsentityid_t child +) { + assertTrue(parent >= 0 && parent < ECS_ENTITY_COUNT_MAX, "Invalid parent ID"); + assertTrue(child >= 0 && child < ECS_ENTITY_COUNT_MAX, "Invalid child ID"); + assertTrue(parent != child, "Child cannot be a child of itself."); + + scenetreenode_t *childNode = &SCENE_TREE.items[child]; + while(childNode) { + if(childNode->parent == parent) return true; + if(childNode->parent == -1) break; // No parent means we've reached the root + childNode = &SCENE_TREE.items[childNode->parent]; + } + + return false; +} \ No newline at end of file diff --git a/src/scene/scenetree.h b/src/scene/scenetree.h new file mode 100644 index 0000000..e6a2320 --- /dev/null +++ b/src/scene/scenetree.h @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "ecs/ecs.h" + +#define SCENE_ITEM_CHILD_MAX 16 + +typedef struct { + ecsentityid_t children[SCENE_ITEM_CHILD_MAX]; + uint8_t childCount; + ecsentityid_t parent; +} scenetreenode_t; + +typedef struct scene_s { + scenetreenode_t items[ECS_ENTITY_COUNT_MAX]; +} scenetree_t; + +extern scenetree_t SCENE_TREE; + +/** + * Initialize the scene tree. + * + * This will initialize the ECS system and prepare the scene tree for use. + */ +void sceneTreeInit(void); + +/** + * Create a new scene item in the scene tree. + * This will create a new entity in the ECS and return its ID. + * + * @return The ID of the newly created scene item. + */ +ecsentityid_t sceneTreeCreate(void); + +/** + * Get the parent of a given scene item. + * + * @param id The ID of the scene item. + * @return The ID of the parent scene item, or -1 if it has no parent. + */ +ecsentityid_t sceneTreeParentGet(const ecsentityid_t child); + +/** + * Get the children of a given scene item. If children is NULL only the count + * will be returned. + * + * @param id The ID of the scene item. + * @param children Pointer to an array where the children IDs will be stored. + * @return The number of children found. + */ +uint8_t sceneTreeChildGetAll(const ecsentityid_t id, ecsentityid_t *children); + +/** + * Add a child to a parent in the scene tree. + * + * @param parent The ID of the parent scene item. + * @param child The ID of the child scene item to add. + */ +void sceneTreeChildAdd(const ecsentityid_t parent, const ecsentityid_t child); + +/** + * Remove a child from a parent in the scene tree. + * + * @param prnt The ID of the parent scene item. + * @param child The ID of the child scene item to remove. + */ +void sceneTreeChildRemove(const ecsentityid_t prnt, const ecsentityid_t child); + +/** + * Remove all children from a parent in the scene tree. + * + * @param parent The ID of the parent scene item. + */ +void sceneTreeChildRemoveAll(const ecsentityid_t parent); + +/** + * Returns true if the child is within the parent's children, recursively. + * + * @param parent The ID of the parent scene item. + * @param child The ID of the child scene item to check. + * @return True if the child is in the parent's children, false otherwise. + */ +bool_t sceneTreeChildInTree( + const ecsentityid_t parent, + const ecsentityid_t child +); \ No newline at end of file