diff --git a/src/dusk/entity/componentlist.h b/src/dusk/entity/componentlist.h index b9c9d228..9c0d2294 100644 --- a/src/dusk/entity/componentlist.h +++ b/src/dusk/entity/componentlist.h @@ -22,3 +22,6 @@ X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL, NULL) X(RENDERABLE, entityrenderable_t, renderable, entityRenderableInit, entityRenderableDispose, NULL) X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, NULL, NULL) + +// Game-specific components +#include "entity/gamecomponentlist.h" diff --git a/src/duskrpg/CMakeLists.txt b/src/duskrpg/CMakeLists.txt index 2d11ff09..9c6131c0 100644 --- a/src/duskrpg/CMakeLists.txt +++ b/src/duskrpg/CMakeLists.txt @@ -9,6 +9,8 @@ target_include_directories(${DUSK_LIBRARY_TARGET_NAME} ${CMAKE_CURRENT_LIST_DIR} ) +add_subdirectory(entity) add_subdirectory(game) add_subdirectory(input) add_subdirectory(item) +add_subdirectory(scene) diff --git a/src/duskrpg/entity/CMakeLists.txt b/src/duskrpg/entity/CMakeLists.txt new file mode 100644 index 00000000..ea2a04bc --- /dev/null +++ b/src/duskrpg/entity/CMakeLists.txt @@ -0,0 +1,8 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Header-only: gamecomponentlist.h is an X-macro list, no implementation. + +add_subdirectory(component) diff --git a/src/duskrpg/entity/component/CMakeLists.txt b/src/duskrpg/entity/component/CMakeLists.txt new file mode 100644 index 00000000..d8d8ff89 --- /dev/null +++ b/src/duskrpg/entity/component/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +add_subdirectory(overworld) diff --git a/src/duskrpg/entity/component/overworld/CMakeLists.txt b/src/duskrpg/entity/component/overworld/CMakeLists.txt new file mode 100644 index 00000000..5b926b93 --- /dev/null +++ b/src/duskrpg/entity/component/overworld/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 + entityplayer.c +) diff --git a/src/duskrpg/entity/component/overworld/entityplayer.c b/src/duskrpg/entity/component/overworld/entityplayer.c new file mode 100644 index 00000000..63ef4991 --- /dev/null +++ b/src/duskrpg/entity/component/overworld/entityplayer.c @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entityplayer.h" +#include "entity/entitymanager.h" +#include "entity/component/physics/entityphysics.h" +#include "entity/component/display/entitycamera.h" +#include "input/input.h" +#include "util/memory.h" + +#define ENTITY_PLAYER_MOVE_SPEED_DEFAULT 4.0f +#define ENTITY_PLAYER_JUMP_IMPULSE_DEFAULT 6.0f + +void entityPlayerInit( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +) { + entityplayer_t *player = entityPlayerGet(mgr, entityId, componentId); + + memoryZero(player, sizeof(entityplayer_t)); + + player->moveSpeed = ENTITY_PLAYER_MOVE_SPEED_DEFAULT; + player->jumpImpulse = ENTITY_PLAYER_JUMP_IMPULSE_DEFAULT; + + entityUpdateAdd(mgr, entityId, entityPlayerUpdate, componentId, NULL); +} + +entityplayer_t *entityPlayerGet( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +) { + return componentGetData(mgr, entityId, componentId, COMPONENT_TYPE_PLAYER); +} + +void entityPlayerUpdate( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + void *user +) { + componentid_t physComp = entityGetComponent( + mgr, entityId, COMPONENT_TYPE_PHYSICS + ); + if(physComp == COMPONENT_ID_INVALID) return; + + entityplayer_t *player = entityPlayerGet(mgr, entityId, componentId); + + vec2 moveInput; + inputAxis2D( + INPUT_BIND_LEFT, INPUT_BIND_RIGHT, + INPUT_BIND_DOWN, INPUT_BIND_UP, + moveInput + ); + + // Move relative to the current camera's horizontal facing, not fixed + // world axes, so "up" always means "away from the camera". + vec2 forward = { 0.0f, -1.0f }; + vec2 right = { 1.0f, 0.0f }; + entityid_t camEntity = entityCameraGetCurrent(mgr); + if(camEntity != ENTITY_ID_INVALID) { + entityCameraGetForward(mgr, camEntity, forward); + entityCameraGetRight(mgr, camEntity, right); + } + + vec2 moveDir = { + right[0] * moveInput[0] + forward[0] * moveInput[1], + right[1] * moveInput[0] + forward[1] * moveInput[1] + }; + + // Clamp to unit length so diagonal input isn't faster than cardinal. + float_t mag = sqrtf(moveDir[0] * moveDir[0] + moveDir[1] * moveDir[1]); + if(mag > 1.0f) { + moveDir[0] /= mag; + moveDir[1] /= mag; + } + + vec3 velocity; + entityPhysicsGetVelocity(mgr, entityId, physComp, velocity); + velocity[0] = moveDir[0] * player->moveSpeed; + velocity[2] = moveDir[1] * player->moveSpeed; + entityPhysicsSetVelocity(mgr, entityId, physComp, velocity); +} diff --git a/src/duskrpg/entity/component/overworld/entityplayer.h b/src/duskrpg/entity/component/overworld/entityplayer.h new file mode 100644 index 00000000..183b9a45 --- /dev/null +++ b/src/duskrpg/entity/component/overworld/entityplayer.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" + +typedef struct { + float_t moveSpeed; + float_t jumpImpulse; +} entityplayer_t; + +/** + * Initializes the player component: sets default move speed and jump + * impulse. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + */ +void entityPlayerInit( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Gets the underlying player structure (temporarily) for the given entity. + * This is really just intended for doing operations faster than using the + * getters and setters, but it is preferred that you use those. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @return The player component data for the given entity and component ID. + */ +entityplayer_t *entityPlayerGet( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Per-tick update for the player component: reads the movement input axes + * and drives the entity's physics velocity, relative to the current + * camera's horizontal facing (so "up" always moves away from the camera + * and "left"/"right" strafe relative to its view), rather than fixed world + * axes. No-op if the entity has no physics component. Registered + * automatically as an update callback by entityPlayerInit. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param user Unused. + */ +void entityPlayerUpdate( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + void *user +); diff --git a/src/duskrpg/entity/gamecomponentlist.h b/src/duskrpg/entity/gamecomponentlist.h new file mode 100644 index 00000000..7592b5b2 --- /dev/null +++ b/src/duskrpg/entity/gamecomponentlist.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +// Game-specific component types, appended after the engine's inbuilt +// components in entity/componentlist.h. +#include "entity/component/overworld/entityplayer.h" + +// Name (Uppercase) +// Structure +// Field name (lowercase) +// Init function (optional) +// Dispose function (optional) +// Render function (optional) + +X(PLAYER, entityplayer_t, player, entityPlayerInit, NULL, NULL) diff --git a/src/duskrpg/game/game.c b/src/duskrpg/game/game.c index 4ea5c851..eed74381 100644 --- a/src/duskrpg/game/game.c +++ b/src/duskrpg/game/game.c @@ -6,8 +6,11 @@ */ #include "game/game.h" +#include "scene/scene.h" +#include "scene/overworldscene.h" errorret_t gameInit(void) { + sceneSetActive(overworldSceneCreate()); errorOk(); } diff --git a/src/duskrpg/scene/CMakeLists.txt b/src/duskrpg/scene/CMakeLists.txt new file mode 100644 index 00000000..06f09ffa --- /dev/null +++ b/src/duskrpg/scene/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 + overworldscene.c +) diff --git a/src/duskrpg/scene/overworldscene.c b/src/duskrpg/scene/overworldscene.c new file mode 100644 index 00000000..5c6dd746 --- /dev/null +++ b/src/duskrpg/scene/overworldscene.c @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "overworldscene.h" +#include "scene/scene.h" +#include "entity/entitymanager.h" +#include "entity/component/display/entityposition.h" +#include "entity/component/display/entitycamera.h" +#include "entity/component/display/entityrenderable.h" +#include "entity/component/physics/entityphysics.h" +#include "display/mesh/plane.h" +#include "display/mesh/capsule.h" +#include "display/color.h" +#include "time/time.h" + +#define OVERWORLD_SCENE_CAMERA_ORBIT_RADIUS 8.0f +#define OVERWORLD_SCENE_CAMERA_ORBIT_HEIGHT 5.0f +#define OVERWORLD_SCENE_CAMERA_ORBIT_SPEED 0.5f + +static overworldcameraorbit_t OVERWORLD_SCENE_CAMERA_ORBIT; + +sceneid_t overworldSceneCreate(void) { + sceneid_t sceneId = sceneCreate(); + entitymanager_t *mgr = sceneGetEntities(sceneId); + + // Camera, orbiting the origin (see overworldSceneCameraOrbitUpdate). + entityid_t camEntity = entityManagerAdd(mgr); + componentid_t camPosition = entityAddComponent( + mgr, camEntity, COMPONENT_TYPE_POSITION + ); + entityAddComponent(mgr, camEntity, COMPONENT_TYPE_CAMERA); + + OVERWORLD_SCENE_CAMERA_ORBIT = (overworldcameraorbit_t){ + .angle = 0.0f, + .radius = OVERWORLD_SCENE_CAMERA_ORBIT_RADIUS, + .height = OVERWORLD_SCENE_CAMERA_ORBIT_HEIGHT, + .speed = OVERWORLD_SCENE_CAMERA_ORBIT_SPEED + }; + entityUpdateAdd( + mgr, camEntity, overworldSceneCameraOrbitUpdate, camPosition, + &OVERWORLD_SCENE_CAMERA_ORBIT + ); + // Position it correctly for the very first rendered frame, rather than + // waiting for the first fixed tick to run. + overworldSceneCameraOrbitUpdate( + mgr, camEntity, camPosition, &OVERWORLD_SCENE_CAMERA_ORBIT + ); + + // Static ground plane. Physics ignores the entity's position component -- + // the shape's own normal/distance fully define the plane in world space. + entityid_t planeEntity = entityManagerAdd(mgr); + componentid_t planePosition = entityAddComponent( + mgr, planeEntity, COMPONENT_TYPE_POSITION + ); + entityPositionSetLocalPosition( + mgr, planeEntity, planePosition, (vec3){ -10.0f, 0.0f, -10.0f } + ); + entityPositionSetLocalScale( + mgr, planeEntity, planePosition, (vec3){ 20.0f, 1.0f, 20.0f } + ); + componentid_t planePhysics = entityAddComponent( + mgr, planeEntity, COMPONENT_TYPE_PHYSICS + ); + entityPhysicsSetBodyType( + mgr, planeEntity, planePhysics, PHYSICS_BODY_STATIC + ); + entityPhysicsSetShape(mgr, planeEntity, planePhysics, (physicsshape_t){ + .type = PHYSICS_SHAPE_PLANE, + .data.plane = { .normal = { 0.0f, 1.0f, 0.0f }, .distance = 0.0f } + }); + componentid_t planeRenderable = entityAddComponent( + mgr, planeEntity, COMPONENT_TYPE_RENDERABLE + ); + entityrenderable_t *planeR = componentGetData( + mgr, planeEntity, planeRenderable, COMPONENT_TYPE_RENDERABLE + ); + planeR->data.material.meshes[0] = &PLANE_MESH_SIMPLE; + entityRenderableSetColor(mgr, planeEntity, planeRenderable, COLOR_GRAY); + + // Player: dynamic capsule body, moved relative to the camera by + // COMPONENT_TYPE_PLAYER's own update callback (see entityplayer.c). + entityid_t playerEntity = entityManagerAdd(mgr); + componentid_t playerPosition = entityAddComponent( + mgr, playerEntity, COMPONENT_TYPE_POSITION + ); + entityPositionSetLocalPosition( + mgr, playerEntity, playerPosition, (vec3){ 0.0f, 2.0f, 0.0f } + ); + componentid_t playerPhysics = entityAddComponent( + mgr, playerEntity, COMPONENT_TYPE_PHYSICS + ); + entityPhysicsSetShape(mgr, playerEntity, playerPhysics, (physicsshape_t){ + .type = PHYSICS_SHAPE_CAPSULE, + .data.capsule = { .radius = 0.5f, .halfHeight = 0.5f } + }); + componentid_t playerRenderable = entityAddComponent( + mgr, playerEntity, COMPONENT_TYPE_RENDERABLE + ); + entityrenderable_t *playerR = componentGetData( + mgr, playerEntity, playerRenderable, COMPONENT_TYPE_RENDERABLE + ); + playerR->data.material.meshes[0] = &CAPSULE_MESH_SIMPLE; + entityRenderableSetColor(mgr, playerEntity, playerRenderable, COLOR_BLUE); + entityAddComponent(mgr, playerEntity, COMPONENT_TYPE_PLAYER); + + return sceneId; +} + +void overworldSceneCameraOrbitUpdate( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + void *user +) { + overworldcameraorbit_t *orbit = (overworldcameraorbit_t *)user; + orbit->angle += TIME.delta * orbit->speed; + + vec3 eye = { + cosf(orbit->angle) * orbit->radius, + orbit->height, + sinf(orbit->angle) * orbit->radius + }; + entityPositionLookAt( + mgr, entityId, componentId, + eye, + (vec3){ 0.0f, 0.0f, 0.0f }, + (vec3){ 0.0f, 1.0f, 0.0f } + ); +} diff --git a/src/duskrpg/scene/overworldscene.h b/src/duskrpg/scene/overworldscene.h new file mode 100644 index 00000000..74ef6d45 --- /dev/null +++ b/src/duskrpg/scene/overworldscene.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "scene/scenebase.h" +#include "entity/entitybase.h" + +typedef struct { + float_t angle; + float_t radius; + float_t height; + float_t speed; +} overworldcameraorbit_t; + +/** + * Creates the overworld scene: a static ground plane, a player entity + * (dynamic capsule body, moved relative to the camera by + * COMPONENT_TYPE_PLAYER's own update callback), and a camera orbiting the + * origin (see overworldSceneCameraOrbitUpdate). Does not make the scene + * active -- call sceneSetActive with the returned ID. + * + * @return The ID of the newly created scene. + */ +sceneid_t overworldSceneCreate(void); + +/** + * Per-tick update for the test camera: orbits it around the world origin + * at a fixed radius/height/speed, always looking back at the origin. + * Registered automatically by overworldSceneCreate. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The camera entity ID. + * @param componentId The camera's position component ID. + * @param user Pointer to this camera's overworldcameraorbit_t state. + */ +void overworldSceneCameraOrbitUpdate( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + void *user +);