test
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"name": "camera",
|
||||
"components": [
|
||||
{
|
||||
"type": "POSITION",
|
||||
"x": 10, "y": 10, "z": 10,
|
||||
"lookAt": { "x": 0, "y": 0, "z": 0 }
|
||||
},
|
||||
{
|
||||
"type": "CAMERA",
|
||||
"projType": "PERSPECTIVE",
|
||||
"nearClip": 0.1, "farClip": 1000,
|
||||
"fov": 0.9
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"components": [
|
||||
{ "type": "POSITION", "x": 0, "y": 1, "z": 0, "parent": "camera" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"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" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"type": "POSITION",
|
||||
"x": 0, "y": 0, "z": 0.85,
|
||||
"parent": "player",
|
||||
"scale": { "x": 0.7, "y": 0.7, "z": 0.7 }
|
||||
},
|
||||
{
|
||||
"type": "RENDERABLE",
|
||||
"renderType": "SHADER_MATERIAL",
|
||||
"priority": 0,
|
||||
"shaderType": "UNLIT",
|
||||
"color": { "r": 80, "g": 160, "b": 255, "a": 255 },
|
||||
"displayState": { "cull": false, "depthTest": true, "blend": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"type": "POSITION",
|
||||
"x": 0, "y": -0.1, "z": 0,
|
||||
"scale": { "x": 20, "y": 0.2, "z": 20 }
|
||||
},
|
||||
{
|
||||
"type": "RENDERABLE",
|
||||
"renderType": "SHADER_MATERIAL",
|
||||
"priority": 0,
|
||||
"shaderType": "UNLIT",
|
||||
"color": { "r": 128, "g": 128, "b": 128, "a": 255 },
|
||||
"displayState": { "cull": false, "depthTest": true, "blend": false }
|
||||
},
|
||||
{
|
||||
"type": "PHYSICS",
|
||||
"bodyType": "STATIC",
|
||||
"shape": {
|
||||
"type": "PLANE",
|
||||
"normalX": 0, "normalY": 1, "normalZ": 0,
|
||||
"distance": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -59,7 +59,8 @@ void planeBuffer(
|
||||
|
||||
switch(axis) {
|
||||
case PLANE_AXIS_XY: {
|
||||
/* Flat in XY at z = min[2]; spans X and Y. */
|
||||
// Flat in XY at z = min[2]; spans X and Y.
|
||||
// +Z normal: CCW when viewed from +Z (matches cube.c's front face).
|
||||
const float_t z = min[2];
|
||||
PLANE_VERT(0, min[0], min[1], z, u0, v0)
|
||||
PLANE_VERT(1, max[0], min[1], z, u1, v0)
|
||||
@@ -71,7 +72,11 @@ void planeBuffer(
|
||||
}
|
||||
|
||||
case PLANE_AXIS_XZ: {
|
||||
/* Flat in XZ at y = min[1]; spans X and Z. */
|
||||
// Flat in XZ at y = min[1]; spans X and Z.
|
||||
// +Y normal: CCW when viewed from +Y (matches cube.c's top face).
|
||||
// X and Z swap handedness relative to XY/YZ (right-hand rule with Y
|
||||
// up), so the corner order here is deliberately not a straight copy
|
||||
// of the XY/YZ pattern -- copying it as-is would flip this to -Y.
|
||||
const float_t y = min[1];
|
||||
PLANE_VERT(0, min[0], y, min[2], u0, v0)
|
||||
PLANE_VERT(1, max[0], y, max[2], u1, v1)
|
||||
@@ -83,7 +88,8 @@ void planeBuffer(
|
||||
}
|
||||
|
||||
case PLANE_AXIS_YZ: {
|
||||
/* Flat in YZ at x = min[0]; spans Y and Z. */
|
||||
// Flat in YZ at x = min[0]; spans Y and Z.
|
||||
// +X normal: CCW when viewed from +X (matches cube.c's right face).
|
||||
const float_t x = min[0];
|
||||
PLANE_VERT(0, x, min[1], min[2], u0, v0)
|
||||
PLANE_VERT(1, x, max[1], min[2], u1, v0)
|
||||
|
||||
@@ -12,13 +12,15 @@
|
||||
componentdefinition_t COMPONENT_DEFINITIONS[] = {
|
||||
[COMPONENT_TYPE_NULL] = { 0 },
|
||||
|
||||
#define X(enm, type, field, iMethod, dMethod, rMethod) \
|
||||
#define X(enm, type, field, iMethod, dMethod, rMethod, sMethod, dsMethod) \
|
||||
[COMPONENT_TYPE_##enm] = { \
|
||||
.enumName = #enm, \
|
||||
.name = #field, \
|
||||
.init = iMethod, \
|
||||
.dispose = dMethod, \
|
||||
.render = rMethod \
|
||||
.render = rMethod, \
|
||||
.serialize = sMethod, \
|
||||
.deserialize = dsMethod \
|
||||
},
|
||||
|
||||
#include "componentlist.h"
|
||||
@@ -150,3 +152,48 @@ void componentDispose(
|
||||
|
||||
cmp->type = COMPONENT_TYPE_NULL;
|
||||
}
|
||||
|
||||
errorret_t componentSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
) {
|
||||
assertNotNull(mgr, "Entity manager cannot be null");
|
||||
assertNotNull(doc, "JSON document cannot be null");
|
||||
assertNotNull(json, "JSON object cannot be null");
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot serialize null component");
|
||||
|
||||
if(!COMPONENT_DEFINITIONS[type].serialize) errorOk();
|
||||
errorChain(
|
||||
COMPONENT_DEFINITIONS[type].serialize(mgr, entityId, componentId, doc,
|
||||
json)
|
||||
);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t componentDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type,
|
||||
yyjson_val *json
|
||||
) {
|
||||
assertNotNull(mgr, "Entity manager cannot be null");
|
||||
assertNotNull(json, "JSON object cannot be null");
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot deserialize null component");
|
||||
|
||||
if(!COMPONENT_DEFINITIONS[type].deserialize) errorOk();
|
||||
errorChain(
|
||||
COMPONENT_DEFINITIONS[type].deserialize(mgr, entityId, componentId, json)
|
||||
);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -7,14 +7,18 @@
|
||||
|
||||
#pragma once
|
||||
#include "entitybase.h"
|
||||
#include "error/error.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define X(enumName, type, field, init, dispose, render) \
|
||||
#define X(enumName, type, field, init, dispose, render, serialize, \
|
||||
deserialize) \
|
||||
// do nothing
|
||||
#include "componentlist.h"
|
||||
#undef X
|
||||
|
||||
typedef union {
|
||||
#define X(enumName, type, field, init, dispose, render) type field;
|
||||
#define X(enumName, type, field, init, dispose, render, serialize, \
|
||||
deserialize) type field;
|
||||
#include "componentlist.h"
|
||||
#undef X
|
||||
} componentdata_t;
|
||||
@@ -25,12 +29,26 @@ typedef struct {
|
||||
void (*init)(entitymanager_t *, const entityid_t, const componentid_t);
|
||||
void (*dispose)(entitymanager_t *, const entityid_t, const componentid_t);
|
||||
errorret_t (*render)(entitymanager_t *, const entityid_t, const componentid_t);
|
||||
errorret_t (*serialize)(
|
||||
entitymanager_t *,
|
||||
const entityid_t,
|
||||
const componentid_t,
|
||||
yyjson_mut_doc *,
|
||||
yyjson_mut_val *
|
||||
);
|
||||
errorret_t (*deserialize)(
|
||||
entitymanager_t *,
|
||||
const entityid_t,
|
||||
const componentid_t,
|
||||
yyjson_val *
|
||||
);
|
||||
} componentdefinition_t;
|
||||
|
||||
typedef enum {
|
||||
COMPONENT_TYPE_NULL,
|
||||
|
||||
#define X(enumName, type, field, init, dispose, render) \
|
||||
#define X(enumName, type, field, init, dispose, render, serialize, \
|
||||
deserialize) \
|
||||
COMPONENT_TYPE_##enumName,
|
||||
#include "componentlist.h"
|
||||
#undef X
|
||||
@@ -127,3 +145,45 @@ void componentDispose(
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t componentRenderAll(entitymanager_t *mgr);
|
||||
|
||||
/**
|
||||
* Writes a component's data into the given JSON object, if the component
|
||||
* type defines a serialize callback. No-op (returns success) for
|
||||
* components whose definition has serialize == NULL.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param type The type of the component to serialize.
|
||||
* @param doc The mutable JSON document to allocate values from.
|
||||
* @param json The JSON object to write the component's fields into.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t componentSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a component's data from the given JSON object, if the component
|
||||
* type defines a deserialize callback. No-op (returns success) for
|
||||
* components whose definition has deserialize == NULL.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param type The type of the component to deserialize.
|
||||
* @param json The JSON object to read the component's fields from.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t componentDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type,
|
||||
yyjson_val *json
|
||||
);
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "entity/entity.h"
|
||||
#include "entity/component/display/entityposition.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void entityCameraInit(
|
||||
entitymanager_t *mgr,
|
||||
@@ -138,3 +140,98 @@ void entityCameraLookAtPixelPerfect(
|
||||
vec3 up = { 0.0f, 0.0f, -1.0f };
|
||||
entityPositionLookAt(mgr, ent, posComp, eye, (float_t *)point, up);
|
||||
}
|
||||
|
||||
errorret_t entityCameraSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
) {
|
||||
entitycamera_t *cam = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
|
||||
);
|
||||
|
||||
const char_t *projTypeName;
|
||||
switch(cam->projType) {
|
||||
case ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE:
|
||||
projTypeName = "PERSPECTIVE";
|
||||
break;
|
||||
case ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED:
|
||||
projTypeName = "PERSPECTIVE_FLIPPED";
|
||||
break;
|
||||
case ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC:
|
||||
projTypeName = "ORTHOGRAPHIC";
|
||||
break;
|
||||
default:
|
||||
assertUnreachable("Unknown camera projection type");
|
||||
}
|
||||
yyjson_mut_obj_add_str(doc, json, "projType", projTypeName);
|
||||
yyjson_mut_obj_add_real(doc, json, "nearClip", cam->nearClip);
|
||||
yyjson_mut_obj_add_real(doc, json, "farClip", cam->farClip);
|
||||
|
||||
if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) {
|
||||
yyjson_mut_obj_add_real(doc, json, "left", cam->orthographic.left);
|
||||
yyjson_mut_obj_add_real(doc, json, "right", cam->orthographic.right);
|
||||
yyjson_mut_obj_add_real(doc, json, "top", cam->orthographic.top);
|
||||
yyjson_mut_obj_add_real(doc, json, "bottom", cam->orthographic.bottom);
|
||||
} else {
|
||||
yyjson_mut_obj_add_real(doc, json, "fov", cam->perspective.fov);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t entityCameraDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_val *json
|
||||
) {
|
||||
entitycamera_t *cam = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
|
||||
);
|
||||
|
||||
yyjson_val *projTypeVal = yyjson_obj_get(json, "projType");
|
||||
if(projTypeVal) {
|
||||
const char_t *projTypeName = yyjson_get_str(projTypeVal);
|
||||
if(stringEquals(projTypeName, "ORTHOGRAPHIC")) {
|
||||
cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC;
|
||||
} else if(stringEquals(projTypeName, "PERSPECTIVE_FLIPPED")) {
|
||||
cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED;
|
||||
} else if(stringEquals(projTypeName, "PERSPECTIVE")) {
|
||||
cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE;
|
||||
} else {
|
||||
errorThrow("Unknown camera projection type '%s'", projTypeName);
|
||||
}
|
||||
}
|
||||
|
||||
yyjson_val *v;
|
||||
if((v = yyjson_obj_get(json, "nearClip"))) {
|
||||
cam->nearClip = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(json, "farClip"))) {
|
||||
cam->farClip = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
|
||||
if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) {
|
||||
if((v = yyjson_obj_get(json, "left"))) {
|
||||
cam->orthographic.left = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(json, "right"))) {
|
||||
cam->orthographic.right = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(json, "top"))) {
|
||||
cam->orthographic.top = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(json, "bottom"))) {
|
||||
cam->orthographic.bottom = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
} else {
|
||||
if((v = yyjson_obj_get(json, "fov"))) {
|
||||
cam->perspective.fov = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#pragma once
|
||||
#include "entity/entitybase.h"
|
||||
#include "error/error.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
typedef enum {
|
||||
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE,
|
||||
@@ -119,3 +121,44 @@ void entityCameraLookAtPixelPerfect(
|
||||
const vec3 eyeOffset,
|
||||
const float_t scale
|
||||
);
|
||||
|
||||
/**
|
||||
* Serializes the camera's projection type ("projType": "PERSPECTIVE" /
|
||||
* "PERSPECTIVE_FLIPPED" / "ORTHOGRAPHIC"), "nearClip", "farClip", and
|
||||
* whichever projection-specific fields apply ("fov" for the perspective
|
||||
* types, "left"/"right"/"top"/"bottom" for orthographic) into the given
|
||||
* JSON object.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param doc The mutable JSON document to allocate values from.
|
||||
* @param json The JSON object to write the component's fields into.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entityCameraSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads "projType", "nearClip", "farClip", and the projection-specific
|
||||
* fields (see entityCameraSerialize()) from the given JSON object and
|
||||
* applies whichever are present, leaving the rest at their current
|
||||
* values.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param json The JSON object to read the component's fields from.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entityCameraDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_val *json
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "entity/entitymanager.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void entityPositionInit(
|
||||
entitymanager_t *mgr,
|
||||
@@ -458,6 +459,17 @@ void entityPositionSetParent(
|
||||
entityPositionMarkDirty(mgr, pos);
|
||||
}
|
||||
|
||||
entityid_t entityPositionGetParent(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
return pos->parentEntityId;
|
||||
}
|
||||
|
||||
entityposition_t *entityPositionGet(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
@@ -655,3 +667,106 @@ void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos) {
|
||||
|
||||
pos->flags &= ~ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
||||
}
|
||||
|
||||
errorret_t entityPositionSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
) {
|
||||
vec3 position, rotation, scale;
|
||||
entityPositionGetLocalPosition(mgr, entityId, componentId, position);
|
||||
entityPositionGetLocalRotation(mgr, entityId, componentId, rotation);
|
||||
entityPositionGetLocalScale(mgr, entityId, componentId, scale);
|
||||
|
||||
yyjson_mut_obj_add_real(doc, json, "x", position[0]);
|
||||
yyjson_mut_obj_add_real(doc, json, "y", position[1]);
|
||||
yyjson_mut_obj_add_real(doc, json, "z", position[2]);
|
||||
|
||||
yyjson_mut_val *rot = yyjson_mut_obj(doc);
|
||||
yyjson_mut_obj_add_val(doc, json, "rotation", rot);
|
||||
yyjson_mut_obj_add_real(doc, rot, "x", rotation[0]);
|
||||
yyjson_mut_obj_add_real(doc, rot, "y", rotation[1]);
|
||||
yyjson_mut_obj_add_real(doc, rot, "z", rotation[2]);
|
||||
|
||||
yyjson_mut_val *scl = yyjson_mut_obj(doc);
|
||||
yyjson_mut_obj_add_val(doc, json, "scale", scl);
|
||||
yyjson_mut_obj_add_real(doc, scl, "x", scale[0]);
|
||||
yyjson_mut_obj_add_real(doc, scl, "y", scale[1]);
|
||||
yyjson_mut_obj_add_real(doc, scl, "z", scale[2]);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t entityPositionDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_val *json
|
||||
) {
|
||||
yyjson_val *xVal = yyjson_obj_get(json, "x");
|
||||
yyjson_val *yVal = yyjson_obj_get(json, "y");
|
||||
yyjson_val *zVal = yyjson_obj_get(json, "z");
|
||||
if(xVal || yVal || zVal) {
|
||||
vec3 position;
|
||||
entityPositionGetLocalPosition(mgr, entityId, componentId, position);
|
||||
if(xVal) position[0] = (float_t)yyjson_get_num(xVal);
|
||||
if(yVal) position[1] = (float_t)yyjson_get_num(yVal);
|
||||
if(zVal) position[2] = (float_t)yyjson_get_num(zVal);
|
||||
entityPositionSetLocalPosition(mgr, entityId, componentId, position);
|
||||
}
|
||||
|
||||
yyjson_val *rot = yyjson_obj_get(json, "rotation");
|
||||
if(rot) {
|
||||
vec3 rotation;
|
||||
entityPositionGetLocalRotation(mgr, entityId, componentId, rotation);
|
||||
yyjson_val *v;
|
||||
if((v = yyjson_obj_get(rot, "x"))) rotation[0] = (float_t)yyjson_get_num(v);
|
||||
if((v = yyjson_obj_get(rot, "y"))) rotation[1] = (float_t)yyjson_get_num(v);
|
||||
if((v = yyjson_obj_get(rot, "z"))) rotation[2] = (float_t)yyjson_get_num(v);
|
||||
entityPositionSetLocalRotation(mgr, entityId, componentId, rotation);
|
||||
}
|
||||
|
||||
yyjson_val *scl = yyjson_obj_get(json, "scale");
|
||||
if(scl) {
|
||||
vec3 scale;
|
||||
entityPositionGetLocalScale(mgr, entityId, componentId, scale);
|
||||
yyjson_val *v;
|
||||
if((v = yyjson_obj_get(scl, "x"))) scale[0] = (float_t)yyjson_get_num(v);
|
||||
if((v = yyjson_obj_get(scl, "y"))) scale[1] = (float_t)yyjson_get_num(v);
|
||||
if((v = yyjson_obj_get(scl, "z"))) scale[2] = (float_t)yyjson_get_num(v);
|
||||
entityPositionSetLocalScale(mgr, entityId, componentId, scale);
|
||||
}
|
||||
|
||||
yyjson_val *lookAt = yyjson_obj_get(json, "lookAt");
|
||||
if(lookAt) {
|
||||
yyjson_val *targetXVal = yyjson_obj_get(lookAt, "x");
|
||||
yyjson_val *targetYVal = yyjson_obj_get(lookAt, "y");
|
||||
yyjson_val *targetZVal = yyjson_obj_get(lookAt, "z");
|
||||
assertTrue(
|
||||
targetXVal && targetYVal && targetZVal,
|
||||
"Position JSON 'lookAt' missing 'x'/'y'/'z' field"
|
||||
);
|
||||
vec3 target = {
|
||||
(float_t)yyjson_get_num(targetXVal),
|
||||
(float_t)yyjson_get_num(targetYVal),
|
||||
(float_t)yyjson_get_num(targetZVal)
|
||||
};
|
||||
|
||||
vec3 up = { 0.0f, 1.0f, 0.0f };
|
||||
yyjson_val *upVal = yyjson_obj_get(lookAt, "up");
|
||||
if(upVal) {
|
||||
yyjson_val *v;
|
||||
if((v = yyjson_obj_get(upVal, "x"))) up[0] = (float_t)yyjson_get_num(v);
|
||||
if((v = yyjson_obj_get(upVal, "y"))) up[1] = (float_t)yyjson_get_num(v);
|
||||
if((v = yyjson_obj_get(upVal, "z"))) up[2] = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
|
||||
vec3 eye;
|
||||
entityPositionGetLocalPosition(mgr, entityId, componentId, eye);
|
||||
entityPositionLookAt(mgr, entityId, componentId, eye, target, up);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#pragma once
|
||||
#include "entity/entitybase.h"
|
||||
#include "error/error.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
/** Maximum number of child position components this node can track. */
|
||||
#define ENTITY_POSITION_CHILDREN_MAX 8
|
||||
@@ -358,6 +360,20 @@ void entityPositionSetParent(
|
||||
const componentid_t parentComponentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the entity ID of this position component's parent.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The parent entity ID, or ENTITY_ID_INVALID if unparented.
|
||||
*/
|
||||
entityid_t entityPositionGetParent(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a direct pointer to the entity position component data.
|
||||
* After modifying localTransform directly, call entityPositionMarkDirty() to
|
||||
@@ -443,3 +459,62 @@ void entityPositionEnsureLocal(entityposition_t *pos);
|
||||
* @param pos The position component to update.
|
||||
*/
|
||||
void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos);
|
||||
|
||||
/**
|
||||
* Serializes the local position as "x"/"y"/"z", and the local rotation
|
||||
* and scale as nested "rotation"/"scale" objects (each with "x"/"y"/"z"),
|
||||
* into the given JSON object. Does NOT write a "parent" field itself --
|
||||
* the parent link (see entityPositionSetParent()) is a cross-entity
|
||||
* reference that only sceneSerialize() has enough context (every
|
||||
* entity's name) to resolve, so it writes "parent" onto this same JSON
|
||||
* object as a sibling field after calling this function.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param doc The mutable JSON document to allocate values from.
|
||||
* @param json The JSON object to write the component's fields into.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entityPositionSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the local position ("x"/"y"/"z") and the local rotation/scale
|
||||
* (nested "rotation"/"scale" objects, each with "x"/"y"/"z") from the
|
||||
* given JSON object and applies whichever fields are present, leaving
|
||||
* the rest at their current values.
|
||||
*
|
||||
* Also supports a nested "lookAt" object as an authoring convenience for
|
||||
* orienting the entity, in place of an explicit "rotation": "x"/"y"/"z"
|
||||
* (required, the world-space point to face) plus an optional nested
|
||||
* "up" ({"x","y","z"}, defaults to {0,1,0}). The eye point is whatever
|
||||
* the local position resolves to after applying "x"/"y"/"z" above (its
|
||||
* current value if none of those were given). If both "rotation" and
|
||||
* "lookAt" are present, "lookAt" wins since it is applied afterward. Not
|
||||
* emitted by entityPositionSerialize() -- lookAt is a one-time input
|
||||
* convenience, not persisted state; it round-trips through "rotation".
|
||||
*
|
||||
* Does NOT read a "parent" field itself, for the same reason
|
||||
* entityPositionSerialize() doesn't write one -- resolving a "parent"
|
||||
* name to an entityId requires sceneDeserialize()'s scene-wide view, so
|
||||
* it calls entityPositionSetParent() directly once every entity in the
|
||||
* scene exists.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param json The JSON object to read the component's fields from.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entityPositionDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_val *json
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "display/display.h"
|
||||
#include "display/mesh/cube.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void entityRenderableInit(
|
||||
@@ -164,3 +165,147 @@ errorret_t entityRenderableDrawCustom(
|
||||
) {
|
||||
return custom->draw(mgr, entityId, componentId, custom->drawUser);
|
||||
}
|
||||
|
||||
errorret_t entityRenderableSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
|
||||
const char_t *renderTypeName;
|
||||
switch(r->type) {
|
||||
case ENTITY_RENDERABLE_TYPE_CUSTOM:
|
||||
renderTypeName = "CUSTOM";
|
||||
break;
|
||||
case ENTITY_RENDERABLE_TYPE_SPRITEBATCH:
|
||||
renderTypeName = "SPRITEBATCH";
|
||||
break;
|
||||
case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL:
|
||||
renderTypeName = "SHADER_MATERIAL";
|
||||
break;
|
||||
default:
|
||||
assertUnreachable("Unknown renderable type");
|
||||
}
|
||||
yyjson_mut_obj_add_str(doc, json, "renderType", renderTypeName);
|
||||
yyjson_mut_obj_add_int(doc, json, "priority", r->priority);
|
||||
|
||||
if(r->type != ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL) errorOk();
|
||||
|
||||
const entityrenderablematerial_t *m = &r->data.material;
|
||||
|
||||
const char_t *shaderTypeName;
|
||||
switch(m->shaderType) {
|
||||
case SHADER_LIST_SHADER_UNLIT:
|
||||
shaderTypeName = "UNLIT";
|
||||
break;
|
||||
default:
|
||||
errorThrow("Cannot serialize unknown shaderType %d", m->shaderType);
|
||||
}
|
||||
yyjson_mut_obj_add_str(doc, json, "shaderType", shaderTypeName);
|
||||
|
||||
yyjson_mut_val *color = yyjson_mut_obj(doc);
|
||||
yyjson_mut_obj_add_val(doc, json, "color", color);
|
||||
yyjson_mut_obj_add_int(doc, color, "r", m->material.unlit.color.r);
|
||||
yyjson_mut_obj_add_int(doc, color, "g", m->material.unlit.color.g);
|
||||
yyjson_mut_obj_add_int(doc, color, "b", m->material.unlit.color.b);
|
||||
yyjson_mut_obj_add_int(doc, color, "a", m->material.unlit.color.a);
|
||||
|
||||
yyjson_mut_val *displayState = yyjson_mut_obj(doc);
|
||||
yyjson_mut_obj_add_val(doc, json, "displayState", displayState);
|
||||
yyjson_mut_obj_add_bool(
|
||||
doc, displayState, "cull", (m->state.flags & DISPLAY_STATE_FLAG_CULL) != 0
|
||||
);
|
||||
yyjson_mut_obj_add_bool(
|
||||
doc, displayState, "depthTest",
|
||||
(m->state.flags & DISPLAY_STATE_FLAG_DEPTH_TEST) != 0
|
||||
);
|
||||
yyjson_mut_obj_add_bool(
|
||||
doc, displayState, "blend",
|
||||
(m->state.flags & DISPLAY_STATE_FLAG_BLEND) != 0
|
||||
);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t entityRenderableDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_val *json
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
|
||||
yyjson_val *renderTypeVal = yyjson_obj_get(json, "renderType");
|
||||
if(renderTypeVal) {
|
||||
const char_t *renderTypeName = yyjson_get_str(renderTypeVal);
|
||||
if(stringEquals(renderTypeName, "CUSTOM")) {
|
||||
r->type = ENTITY_RENDERABLE_TYPE_CUSTOM;
|
||||
} else if(stringEquals(renderTypeName, "SPRITEBATCH")) {
|
||||
r->type = ENTITY_RENDERABLE_TYPE_SPRITEBATCH;
|
||||
} else if(stringEquals(renderTypeName, "SHADER_MATERIAL")) {
|
||||
r->type = ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL;
|
||||
} else {
|
||||
errorThrow("Unknown renderable renderType '%s'", renderTypeName);
|
||||
}
|
||||
}
|
||||
|
||||
yyjson_val *priorityVal = yyjson_obj_get(json, "priority");
|
||||
if(priorityVal) r->priority = (int8_t)yyjson_get_int(priorityVal);
|
||||
|
||||
if(r->type != ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL) errorOk();
|
||||
|
||||
entityrenderablematerial_t *m = &r->data.material;
|
||||
|
||||
yyjson_val *shaderTypeVal = yyjson_obj_get(json, "shaderType");
|
||||
if(shaderTypeVal) {
|
||||
const char_t *shaderTypeName = yyjson_get_str(shaderTypeVal);
|
||||
if(stringEquals(shaderTypeName, "UNLIT")) {
|
||||
m->shaderType = SHADER_LIST_SHADER_UNLIT;
|
||||
} else {
|
||||
errorThrow("Unknown renderable shaderType '%s'", shaderTypeName);
|
||||
}
|
||||
}
|
||||
|
||||
yyjson_val *color = yyjson_obj_get(json, "color");
|
||||
if(color) {
|
||||
yyjson_val *v;
|
||||
if((v = yyjson_obj_get(color, "r"))) {
|
||||
m->material.unlit.color.r = (colorchannel8_t)yyjson_get_int(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(color, "g"))) {
|
||||
m->material.unlit.color.g = (colorchannel8_t)yyjson_get_int(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(color, "b"))) {
|
||||
m->material.unlit.color.b = (colorchannel8_t)yyjson_get_int(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(color, "a"))) {
|
||||
m->material.unlit.color.a = (colorchannel8_t)yyjson_get_int(v);
|
||||
}
|
||||
}
|
||||
|
||||
yyjson_val *displayState = yyjson_obj_get(json, "displayState");
|
||||
if(displayState) {
|
||||
yyjson_val *v;
|
||||
if((v = yyjson_obj_get(displayState, "cull"))) {
|
||||
if(yyjson_get_bool(v)) m->state.flags |= DISPLAY_STATE_FLAG_CULL;
|
||||
else m->state.flags &= ~DISPLAY_STATE_FLAG_CULL;
|
||||
}
|
||||
if((v = yyjson_obj_get(displayState, "depthTest"))) {
|
||||
if(yyjson_get_bool(v)) m->state.flags |= DISPLAY_STATE_FLAG_DEPTH_TEST;
|
||||
else m->state.flags &= ~DISPLAY_STATE_FLAG_DEPTH_TEST;
|
||||
}
|
||||
if((v = yyjson_obj_get(displayState, "blend"))) {
|
||||
if(yyjson_get_bool(v)) m->state.flags |= DISPLAY_STATE_FLAG_BLEND;
|
||||
else m->state.flags &= ~DISPLAY_STATE_FLAG_BLEND;
|
||||
}
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "display/shader/shadermaterial.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/displaystate.h"
|
||||
#include "error/error.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX 64
|
||||
#define ENTITY_RENDERABLE_MESHES_MAX 8
|
||||
@@ -211,3 +213,47 @@ errorret_t entityRenderableDrawCustom(
|
||||
const componentid_t componentId,
|
||||
const entityrenderablecustom_t *custom
|
||||
);
|
||||
|
||||
/**
|
||||
* Serializes the renderable's "renderType"
|
||||
* ("CUSTOM"/"SPRITEBATCH"/"SHADER_MATERIAL") and "priority" into the
|
||||
* given JSON object. For ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL, also
|
||||
* serializes "shaderType" (currently only "UNLIT"), its "color"
|
||||
* ({"r","g","b","a"}, 0-255), and "displayState" (an object of
|
||||
* "cull"/"depthTest"/"blend" booleans). Meshes, spritebatch
|
||||
* textures/sprites, and the custom draw callback are asset-loaded or
|
||||
* runtime-only and are not serialized.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param doc The mutable JSON document to allocate values from.
|
||||
* @param json The JSON object to write the component's fields into.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entityRenderableSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads "renderType", "priority", and (for SHADER_MATERIAL)
|
||||
* "shaderType"/"color"/"displayState" (see entityRenderableSerialize())
|
||||
* from the given JSON object and applies whichever are present, leaving
|
||||
* the rest at their current values.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param json The JSON object to read the component's fields from.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entityRenderableDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_val *json
|
||||
);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "entityphysics.h"
|
||||
#include "entity/entitymanager.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void entityPhysicsInit(
|
||||
entitymanager_t *mgr,
|
||||
@@ -113,3 +115,198 @@ physicsbodytype_t entityPhysicsGetBodyType(
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
return phys->type;
|
||||
}
|
||||
|
||||
errorret_t entityPhysicsSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
|
||||
const char_t *bodyTypeName;
|
||||
switch(phys->type) {
|
||||
case PHYSICS_BODY_STATIC:
|
||||
bodyTypeName = "STATIC";
|
||||
break;
|
||||
case PHYSICS_BODY_DYNAMIC:
|
||||
bodyTypeName = "DYNAMIC";
|
||||
break;
|
||||
case PHYSICS_BODY_KINEMATIC:
|
||||
bodyTypeName = "KINEMATIC";
|
||||
break;
|
||||
default:
|
||||
assertUnreachable("Unknown physics body type");
|
||||
}
|
||||
yyjson_mut_obj_add_str(doc, json, "bodyType", bodyTypeName);
|
||||
|
||||
yyjson_mut_val *shape = yyjson_mut_obj(doc);
|
||||
yyjson_mut_obj_add_val(doc, json, "shape", shape);
|
||||
|
||||
switch(phys->shape.type) {
|
||||
case PHYSICS_SHAPE_CUBE:
|
||||
yyjson_mut_obj_add_str(doc, shape, "type", "CUBE");
|
||||
yyjson_mut_obj_add_real(
|
||||
doc, shape, "halfExtentX", phys->shape.data.cube.halfExtents[0]
|
||||
);
|
||||
yyjson_mut_obj_add_real(
|
||||
doc, shape, "halfExtentY", phys->shape.data.cube.halfExtents[1]
|
||||
);
|
||||
yyjson_mut_obj_add_real(
|
||||
doc, shape, "halfExtentZ", phys->shape.data.cube.halfExtents[2]
|
||||
);
|
||||
break;
|
||||
case PHYSICS_SHAPE_SPHERE:
|
||||
yyjson_mut_obj_add_str(doc, shape, "type", "SPHERE");
|
||||
yyjson_mut_obj_add_real(
|
||||
doc, shape, "radius", phys->shape.data.sphere.radius
|
||||
);
|
||||
break;
|
||||
case PHYSICS_SHAPE_CAPSULE:
|
||||
yyjson_mut_obj_add_str(doc, shape, "type", "CAPSULE");
|
||||
yyjson_mut_obj_add_real(
|
||||
doc, shape, "radius", phys->shape.data.capsule.radius
|
||||
);
|
||||
yyjson_mut_obj_add_real(
|
||||
doc, shape, "halfHeight", phys->shape.data.capsule.halfHeight
|
||||
);
|
||||
break;
|
||||
case PHYSICS_SHAPE_PLANE:
|
||||
yyjson_mut_obj_add_str(doc, shape, "type", "PLANE");
|
||||
yyjson_mut_obj_add_real(
|
||||
doc, shape, "normalX", phys->shape.data.plane.normal[0]
|
||||
);
|
||||
yyjson_mut_obj_add_real(
|
||||
doc, shape, "normalY", phys->shape.data.plane.normal[1]
|
||||
);
|
||||
yyjson_mut_obj_add_real(
|
||||
doc, shape, "normalZ", phys->shape.data.plane.normal[2]
|
||||
);
|
||||
yyjson_mut_obj_add_real(
|
||||
doc, shape, "distance", phys->shape.data.plane.distance
|
||||
);
|
||||
break;
|
||||
case PHYSICS_SHAPE_CUSTOM:
|
||||
errorThrow(
|
||||
"Cannot serialize a PHYSICS_SHAPE_CUSTOM shape (callback/userData "
|
||||
"are not representable in JSON)"
|
||||
);
|
||||
default:
|
||||
assertUnreachable("Unknown physics shape type");
|
||||
}
|
||||
|
||||
yyjson_mut_obj_add_real(doc, json, "velocityX", phys->velocity[0]);
|
||||
yyjson_mut_obj_add_real(doc, json, "velocityY", phys->velocity[1]);
|
||||
yyjson_mut_obj_add_real(doc, json, "velocityZ", phys->velocity[2]);
|
||||
yyjson_mut_obj_add_real(doc, json, "gravityScale", phys->gravityScale);
|
||||
yyjson_mut_obj_add_bool(doc, json, "onGround", phys->onGround);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t entityPhysicsDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_val *json
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
|
||||
yyjson_val *bodyTypeVal = yyjson_obj_get(json, "bodyType");
|
||||
if(bodyTypeVal) {
|
||||
const char_t *bodyTypeName = yyjson_get_str(bodyTypeVal);
|
||||
if(stringEquals(bodyTypeName, "STATIC")) {
|
||||
phys->type = PHYSICS_BODY_STATIC;
|
||||
} else if(stringEquals(bodyTypeName, "DYNAMIC")) {
|
||||
phys->type = PHYSICS_BODY_DYNAMIC;
|
||||
} else if(stringEquals(bodyTypeName, "KINEMATIC")) {
|
||||
phys->type = PHYSICS_BODY_KINEMATIC;
|
||||
} else {
|
||||
errorThrow("Unknown physics body type '%s'", bodyTypeName);
|
||||
}
|
||||
}
|
||||
|
||||
yyjson_val *shape = yyjson_obj_get(json, "shape");
|
||||
if(shape) {
|
||||
yyjson_val *shapeTypeVal = yyjson_obj_get(shape, "type");
|
||||
assertNotNull(shapeTypeVal, "Physics shape JSON missing 'type' field");
|
||||
const char_t *shapeTypeName = yyjson_get_str(shapeTypeVal);
|
||||
yyjson_val *v;
|
||||
|
||||
if(stringEquals(shapeTypeName, "CUBE")) {
|
||||
physicsshape_t s = { .type = PHYSICS_SHAPE_CUBE };
|
||||
if((v = yyjson_obj_get(shape, "halfExtentX"))) {
|
||||
s.data.cube.halfExtents[0] = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(shape, "halfExtentY"))) {
|
||||
s.data.cube.halfExtents[1] = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(shape, "halfExtentZ"))) {
|
||||
s.data.cube.halfExtents[2] = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
entityPhysicsSetShape(mgr, entityId, componentId, s);
|
||||
} else if(stringEquals(shapeTypeName, "SPHERE")) {
|
||||
physicsshape_t s = { .type = PHYSICS_SHAPE_SPHERE };
|
||||
if((v = yyjson_obj_get(shape, "radius"))) {
|
||||
s.data.sphere.radius = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
entityPhysicsSetShape(mgr, entityId, componentId, s);
|
||||
} else if(stringEquals(shapeTypeName, "CAPSULE")) {
|
||||
physicsshape_t s = { .type = PHYSICS_SHAPE_CAPSULE };
|
||||
if((v = yyjson_obj_get(shape, "radius"))) {
|
||||
s.data.capsule.radius = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(shape, "halfHeight"))) {
|
||||
s.data.capsule.halfHeight = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
entityPhysicsSetShape(mgr, entityId, componentId, s);
|
||||
} else if(stringEquals(shapeTypeName, "PLANE")) {
|
||||
physicsshape_t s = { .type = PHYSICS_SHAPE_PLANE };
|
||||
if((v = yyjson_obj_get(shape, "normalX"))) {
|
||||
s.data.plane.normal[0] = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(shape, "normalY"))) {
|
||||
s.data.plane.normal[1] = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(shape, "normalZ"))) {
|
||||
s.data.plane.normal[2] = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(shape, "distance"))) {
|
||||
s.data.plane.distance = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
entityPhysicsSetShape(mgr, entityId, componentId, s);
|
||||
} else {
|
||||
errorThrow("Unknown physics shape type '%s'", shapeTypeName);
|
||||
}
|
||||
}
|
||||
|
||||
yyjson_val *v;
|
||||
vec3 velocity;
|
||||
entityPhysicsGetVelocity(mgr, entityId, componentId, velocity);
|
||||
bool_t velocityChanged = false;
|
||||
if((v = yyjson_obj_get(json, "velocityX"))) {
|
||||
velocity[0] = (float_t)yyjson_get_num(v);
|
||||
velocityChanged = true;
|
||||
}
|
||||
if((v = yyjson_obj_get(json, "velocityY"))) {
|
||||
velocity[1] = (float_t)yyjson_get_num(v);
|
||||
velocityChanged = true;
|
||||
}
|
||||
if((v = yyjson_obj_get(json, "velocityZ"))) {
|
||||
velocity[2] = (float_t)yyjson_get_num(v);
|
||||
velocityChanged = true;
|
||||
}
|
||||
if(velocityChanged) {
|
||||
entityPhysicsSetVelocity(mgr, entityId, componentId, velocity);
|
||||
}
|
||||
|
||||
if((v = yyjson_obj_get(json, "gravityScale"))) {
|
||||
phys->gravityScale = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(json, "onGround"))) {
|
||||
phys->onGround = yyjson_get_bool(v);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "entity/entitybase.h"
|
||||
#include "physics/physicsshape.h"
|
||||
#include "physics/physicsbodytype.h"
|
||||
#include "error/error.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
typedef struct {
|
||||
physicsbodytype_t type;
|
||||
@@ -170,3 +172,45 @@ physicsbodytype_t entityPhysicsGetBodyType(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Serializes the physics body into the given JSON object: "bodyType"
|
||||
* ("STATIC"/"DYNAMIC"/"KINEMATIC"), "shape" (an object with its own
|
||||
* "type" of "CUBE"/"SPHERE"/"CAPSULE"/"PLANE" plus that shape's fields),
|
||||
* "velocity" ({"x","y","z"}), "gravityScale", and "onGround". Fails if
|
||||
* the body's shape is PHYSICS_SHAPE_CUSTOM -- custom shapes carry a
|
||||
* callback and opaque userData pointer that cannot be represented in
|
||||
* JSON.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param doc The mutable JSON document to allocate values from.
|
||||
* @param json The JSON object to write the component's fields into.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entityPhysicsSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads "bodyType", "shape", "velocity", "gravityScale", and "onGround"
|
||||
* (see entityPhysicsSerialize()) from the given JSON object and applies
|
||||
* whichever are present, leaving the rest at their current values.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param json The JSON object to read the component's fields from.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entityPhysicsDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_val *json
|
||||
);
|
||||
|
||||
@@ -16,12 +16,18 @@
|
||||
// Init function (optional)
|
||||
// Dispose function (optional)
|
||||
// Render function (optional)
|
||||
// Serialize function (optional)
|
||||
// Deserialize function (optional)
|
||||
|
||||
X(POSITION, entityposition_t, position, entityPositionInit, NULL, NULL)
|
||||
X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL, NULL)
|
||||
X(POSITION, entityposition_t, position, entityPositionInit, NULL, NULL,
|
||||
entityPositionSerialize, entityPositionDeserialize)
|
||||
X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL, NULL,
|
||||
entityCameraSerialize, entityCameraDeserialize)
|
||||
X(RENDERABLE, entityrenderable_t, renderable,
|
||||
entityRenderableInit, entityRenderableDispose, NULL)
|
||||
X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, NULL, NULL)
|
||||
entityRenderableInit, entityRenderableDispose, NULL,
|
||||
entityRenderableSerialize, entityRenderableDeserialize)
|
||||
X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, NULL, NULL,
|
||||
entityPhysicsSerialize, entityPhysicsDeserialize)
|
||||
|
||||
// Game-specific components
|
||||
#include "entity/gamecomponentlist.h"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "entitymanager.h"
|
||||
#include "component/display/entityposition.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void entityInit(entitymanager_t *mgr, const entityid_t entityId) {
|
||||
@@ -72,6 +73,32 @@ componentid_t entityGetComponent(
|
||||
return compId;
|
||||
}
|
||||
|
||||
void entitySetName(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const char_t *name
|
||||
) {
|
||||
stringCopy(mgr->entities[entityId].name, name, ENTITY_NAME_MAX);
|
||||
}
|
||||
|
||||
const char_t *entityGetName(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId
|
||||
) {
|
||||
return mgr->entities[entityId].name;
|
||||
}
|
||||
|
||||
entityid_t entityFindByName(
|
||||
entitymanager_t *mgr,
|
||||
const char_t *name
|
||||
) {
|
||||
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||
if(!(mgr->entities[i].state & ENTITY_STATE_ACTIVE)) continue;
|
||||
if(stringEquals(mgr->entities[i].name, name)) return i;
|
||||
}
|
||||
return ENTITY_ID_INVALID;
|
||||
}
|
||||
|
||||
void entityDisposeDeep(entitymanager_t *mgr, const entityid_t entityId) {
|
||||
componentid_t posComp = entityGetComponent(
|
||||
mgr, entityId, COMPONENT_TYPE_POSITION
|
||||
@@ -186,3 +213,79 @@ void entityDisposeRemove(
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t entitySerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
) {
|
||||
assertNotNull(mgr, "Entity manager cannot be null");
|
||||
assertNotNull(doc, "JSON document cannot be null");
|
||||
assertNotNull(json, "JSON object cannot be null");
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
|
||||
const char_t *name = mgr->entities[entityId].name;
|
||||
if(name[0] != '\0') yyjson_mut_obj_add_strcpy(doc, json, "name", name);
|
||||
|
||||
yyjson_mut_val *components = yyjson_mut_arr(doc);
|
||||
yyjson_mut_obj_add_val(doc, json, "components", components);
|
||||
|
||||
for(componenttype_t type = 1; type < COMPONENT_TYPE_COUNT; type++) {
|
||||
componentid_t componentId = entityGetComponent(mgr, entityId, type);
|
||||
if(componentId == COMPONENT_ID_INVALID) continue;
|
||||
|
||||
yyjson_mut_val *component = yyjson_mut_obj(doc);
|
||||
yyjson_mut_arr_add_val(components, component);
|
||||
yyjson_mut_obj_add_str(
|
||||
doc, component, "type", COMPONENT_DEFINITIONS[type].enumName
|
||||
);
|
||||
|
||||
errorChain(
|
||||
componentSerialize(mgr, entityId, componentId, type, doc, component)
|
||||
);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t entityDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
yyjson_val *json
|
||||
) {
|
||||
assertNotNull(mgr, "Entity manager cannot be null");
|
||||
assertNotNull(json, "JSON object cannot be null");
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
|
||||
yyjson_val *nameVal = yyjson_obj_get(json, "name");
|
||||
if(nameVal) entitySetName(mgr, entityId, yyjson_get_str(nameVal));
|
||||
|
||||
yyjson_val *components = yyjson_obj_get(json, "components");
|
||||
if(!components) errorOk();
|
||||
|
||||
size_t idx, max;
|
||||
yyjson_val *component;
|
||||
yyjson_arr_foreach(components, idx, max, component) {
|
||||
yyjson_val *typeVal = yyjson_obj_get(component, "type");
|
||||
assertNotNull(typeVal, "Component JSON entry missing 'type' field");
|
||||
const char_t *typeName = yyjson_get_str(typeVal);
|
||||
|
||||
componenttype_t type = COMPONENT_TYPE_NULL;
|
||||
for(componenttype_t i = 1; i < COMPONENT_TYPE_COUNT; i++) {
|
||||
if(!stringEquals(COMPONENT_DEFINITIONS[i].enumName, typeName)) continue;
|
||||
type = i;
|
||||
break;
|
||||
}
|
||||
if(type == COMPONENT_TYPE_NULL) {
|
||||
errorThrow("Unknown component type '%s'", typeName);
|
||||
}
|
||||
|
||||
componentid_t componentId = entityAddComponent(mgr, entityId, type);
|
||||
errorChain(
|
||||
componentDeserialize(mgr, entityId, componentId, type, component)
|
||||
);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
|
||||
#pragma once
|
||||
#include "component.h"
|
||||
#include "error/error.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define ENTITY_STATE_ACTIVE (1 << 0)
|
||||
|
||||
#define ENTITY_UPDATE_CALLBACK_COUNT_MAX 5
|
||||
#define ENTITY_DISPOSE_CALLBACK_COUNT_MAX 5
|
||||
#define ENTITY_NAME_MAX 32
|
||||
|
||||
typedef void (*entitycallback_t)(
|
||||
entitymanager_t *mgr,
|
||||
@@ -21,6 +24,7 @@ typedef void (*entitycallback_t)(
|
||||
);
|
||||
|
||||
typedef struct {
|
||||
char_t name[ENTITY_NAME_MAX];
|
||||
uint8_t state;
|
||||
uint8_t updateCount;
|
||||
uint8_t disposeCount;
|
||||
@@ -69,6 +73,47 @@ componentid_t entityGetComponent(
|
||||
const componenttype_t type
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets an entity's name. Names are optional -- an entity's name is empty
|
||||
* by default (entityInit zeroes it) -- but are required to make the
|
||||
* entity referenceable by name, e.g. as a parent in POSITION's "parent"
|
||||
* JSON field (see entityPositionSetParent()).
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The ID of the entity to name.
|
||||
* @param name The name to give the entity. Must be shorter than
|
||||
* ENTITY_NAME_MAX.
|
||||
*/
|
||||
void entitySetName(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const char_t *name
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets an entity's name, or an empty string if it has none.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The ID of the entity to get the name of.
|
||||
* @return The entity's name.
|
||||
*/
|
||||
const char_t *entityGetName(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId
|
||||
);
|
||||
|
||||
/**
|
||||
* Finds the first active entity with the given name.
|
||||
*
|
||||
* @param mgr The entity manager to search.
|
||||
* @param name The name to search for.
|
||||
* @return The entity ID, or ENTITY_ID_INVALID if no active entity matches.
|
||||
*/
|
||||
entityid_t entityFindByName(
|
||||
entitymanager_t *mgr,
|
||||
const char_t *name
|
||||
);
|
||||
|
||||
/**
|
||||
* Runs all registered update callbacks for the entity.
|
||||
*
|
||||
@@ -156,3 +201,47 @@ void entityDisposeRemove(
|
||||
const entityid_t entityId,
|
||||
const entitycallback_t callback
|
||||
);
|
||||
|
||||
/**
|
||||
* Serializes an entity's name (if set) and components into the given
|
||||
* JSON object, writing an optional "name" string and a "components"
|
||||
* array of { "type": <enumName>, ...fields } entries. Only component
|
||||
* types the entity actually has are written; each entry's extra fields
|
||||
* come from that component type's serialize callback (see
|
||||
* componentSerialize()). "name" is omitted entirely for unnamed
|
||||
* entities.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The ID of the entity to serialize.
|
||||
* @param doc The mutable JSON document to allocate values from.
|
||||
* @param json The JSON object to write the entity's "name"/"components"
|
||||
* keys into.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entitySerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entityDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
yyjson_val *json
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "scene.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "time/time.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
@@ -190,3 +191,134 @@ errorret_t sceneDispose(void) {
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneSerialize(
|
||||
const sceneid_t id,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
) {
|
||||
assertTrue(id < SCENE_COUNT_MAX, "Scene ID OOB");
|
||||
assertTrue(SCENE_MANAGER.scenes[id].used, "Scene is not in use");
|
||||
assertNotNull(doc, "JSON document cannot be null");
|
||||
assertNotNull(json, "JSON object cannot be null");
|
||||
|
||||
entitymanager_t *mgr = &SCENE_MANAGER.scenes[id].entities;
|
||||
|
||||
yyjson_mut_val *entities = yyjson_mut_arr(doc);
|
||||
yyjson_mut_obj_add_val(doc, json, "entities", entities);
|
||||
|
||||
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||
if(!(mgr->entities[i].state & ENTITY_STATE_ACTIVE)) continue;
|
||||
|
||||
yyjson_mut_val *entity = yyjson_mut_obj(doc);
|
||||
yyjson_mut_arr_add_val(entities, entity);
|
||||
|
||||
errorChain(entitySerialize(mgr, i, doc, entity));
|
||||
|
||||
// entityPositionSerialize() doesn't know about sibling entities' names,
|
||||
// so the "parent" cross-reference is written here instead, onto the
|
||||
// POSITION object entitySerialize() just built.
|
||||
componentid_t posComp = entityGetComponent(mgr, i, COMPONENT_TYPE_POSITION);
|
||||
if(posComp == COMPONENT_ID_INVALID) continue;
|
||||
entityid_t parentEntityId = entityPositionGetParent(mgr, i, posComp);
|
||||
if(parentEntityId == ENTITY_ID_INVALID) continue;
|
||||
const char_t *parentName = entityGetName(mgr, parentEntityId);
|
||||
if(parentName[0] == '\0') continue;
|
||||
|
||||
yyjson_mut_val *components = yyjson_mut_obj_get(entity, "components");
|
||||
size_t compIdx, compMax;
|
||||
yyjson_mut_val *component;
|
||||
yyjson_mut_arr_foreach(components, compIdx, compMax, component) {
|
||||
yyjson_mut_val *typeVal = yyjson_mut_obj_get(component, "type");
|
||||
if(!stringEquals(yyjson_mut_get_str(typeVal), "POSITION")) continue;
|
||||
yyjson_mut_obj_add_strcpy(doc, component, "parent", parentName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneDeserialize(
|
||||
const sceneid_t id,
|
||||
yyjson_val *json
|
||||
) {
|
||||
assertTrue(id < SCENE_COUNT_MAX, "Scene ID OOB");
|
||||
assertTrue(SCENE_MANAGER.scenes[id].used, "Scene is not in use");
|
||||
assertNotNull(json, "JSON object cannot be null");
|
||||
|
||||
entitymanager_t *mgr = &SCENE_MANAGER.scenes[id].entities;
|
||||
|
||||
yyjson_val *entities = yyjson_obj_get(json, "entities");
|
||||
if(!entities) errorOk();
|
||||
|
||||
entityid_t indexToEntityId[ENTITY_COUNT_MAX];
|
||||
size_t idx, max;
|
||||
yyjson_val *entity;
|
||||
yyjson_arr_foreach(entities, idx, max, entity) {
|
||||
entityid_t entityId = entityManagerAdd(mgr);
|
||||
indexToEntityId[idx] = entityId;
|
||||
errorChain(entityDeserialize(mgr, entityId, entity));
|
||||
}
|
||||
|
||||
// Second pass: every entity now exists (and is findable by name), so
|
||||
// POSITION's "parent" references (see entityPositionDeserialize()) can
|
||||
// be resolved regardless of which order parent/child appeared in.
|
||||
yyjson_arr_foreach(entities, idx, max, entity) {
|
||||
errorChain(sceneDeserializeResolveParent(
|
||||
mgr, indexToEntityId[idx], entity
|
||||
));
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneDeserializeResolveParent(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
yyjson_val *entityJson
|
||||
) {
|
||||
yyjson_val *components = yyjson_obj_get(entityJson, "components");
|
||||
if(!components) errorOk();
|
||||
|
||||
yyjson_val *positionObj = NULL;
|
||||
size_t idx, max;
|
||||
yyjson_val *component;
|
||||
yyjson_arr_foreach(components, idx, max, component) {
|
||||
yyjson_val *typeVal = yyjson_obj_get(component, "type");
|
||||
if(!stringEquals(yyjson_get_str(typeVal), "POSITION")) continue;
|
||||
positionObj = component;
|
||||
break;
|
||||
}
|
||||
if(!positionObj) errorOk();
|
||||
|
||||
yyjson_val *parentVal = yyjson_obj_get(positionObj, "parent");
|
||||
if(!parentVal) errorOk();
|
||||
|
||||
const char_t *parentName = yyjson_get_str(parentVal);
|
||||
entityid_t parentEntityId = entityFindByName(mgr, parentName);
|
||||
if(parentEntityId == ENTITY_ID_INVALID || parentEntityId == entityId) {
|
||||
errorThrow("POSITION 'parent' references unknown entity '%s'", parentName);
|
||||
}
|
||||
|
||||
componentid_t componentId = entityGetComponent(
|
||||
mgr, entityId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
componentid_t parentComponentId = entityGetComponent(
|
||||
mgr, parentEntityId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
if(
|
||||
componentId == COMPONENT_ID_INVALID ||
|
||||
parentComponentId == COMPONENT_ID_INVALID
|
||||
) {
|
||||
errorThrow(
|
||||
"POSITION 'parent' reference '%s' requires both entities to have a "
|
||||
"POSITION component", parentName
|
||||
);
|
||||
}
|
||||
|
||||
entityPositionSetParent(
|
||||
mgr, entityId, componentId, parentEntityId, parentComponentId
|
||||
);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "scenebase.h"
|
||||
#include "entity/entitymanager.h"
|
||||
#include "physics/physicsworld.h"
|
||||
#include "error/error.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
typedef struct {
|
||||
bool_t used;
|
||||
@@ -119,3 +121,58 @@ errorret_t sceneRender(void);
|
||||
* @return An error if the dispose failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneDispose(void);
|
||||
|
||||
/**
|
||||
* Serializes a scene's active entities into the given JSON object,
|
||||
* writing an "entities" array of objects (see entitySerialize()). Also
|
||||
* writes a "parent" name reference onto any entity's POSITION object
|
||||
* whose entityPositionGetParent() resolves to a named entity (silently
|
||||
* omitted if the parent has no name -- see entitySetName()).
|
||||
*
|
||||
* @param id The ID of the scene to serialize.
|
||||
* @param doc The mutable JSON document to allocate values from.
|
||||
* @param json The JSON object to write the scene's "entities" key into.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t sceneSerialize(
|
||||
const sceneid_t id,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
);
|
||||
|
||||
/**
|
||||
* Deserializes entities from the given JSON object's "entities" array into
|
||||
* a scene, creating one new entity per array entry (see
|
||||
* entityDeserialize()), then resolves every entity's POSITION "parent"
|
||||
* name reference (see sceneDeserializeResolveParent()) now that all
|
||||
* entities exist and are named. No-op if json has no "entities" array.
|
||||
*
|
||||
* @param id The ID of the scene to deserialize into.
|
||||
* @param json The JSON object to read the scene's "entities" key from.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t sceneDeserialize(
|
||||
const sceneid_t id,
|
||||
yyjson_val *json
|
||||
);
|
||||
|
||||
/**
|
||||
* Internal. Resolves a single entity's POSITION "parent" reference (a
|
||||
* name string, see entityPositionDeserialize()), if present, into a real
|
||||
* parent link via entityPositionSetParent(). Called by sceneDeserialize()
|
||||
* once every entity in the scene has been created and named, since
|
||||
* resolving a name to an entityId requires entityFindByName() to be able
|
||||
* to see the whole scene. No-op if entityJson has no POSITION component
|
||||
* or its POSITION has no "parent" field.
|
||||
*
|
||||
* @param mgr The entity manager the entity belongs to.
|
||||
* @param entityId The entity ID to resolve the parent reference for.
|
||||
* @param entityJson The entity's own JSON object (as found in the
|
||||
* scene's "entities" array).
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t sceneDeserializeResolveParent(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
yyjson_val *entityJson
|
||||
);
|
||||
|
||||
@@ -9,12 +9,18 @@
|
||||
#include "entity/entitymanager.h"
|
||||
#include "entity/component/physics/entityphysics.h"
|
||||
#include "entity/component/display/entitycamera.h"
|
||||
#include "entity/component/display/entityposition.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
|
||||
|
||||
// Below this squared magnitude, movement input is treated as "not moving"
|
||||
// and the player keeps facing whichever way it last faced, rather than
|
||||
// snapping to some default orientation.
|
||||
#define ENTITY_PLAYER_MOVE_ROTATE_THRESHOLD_SQ 0.0001f
|
||||
|
||||
void entityPlayerInit(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
@@ -85,4 +91,58 @@ void entityPlayerUpdate(
|
||||
velocity[0] = moveDir[0] * player->moveSpeed;
|
||||
velocity[2] = moveDir[1] * player->moveSpeed;
|
||||
entityPhysicsSetVelocity(mgr, entityId, physComp, velocity);
|
||||
|
||||
// Face the direction of movement. Only updated while actually moving --
|
||||
// releasing input leaves the player facing whichever way it last moved.
|
||||
float_t moveDirMagSq = moveDir[0] * moveDir[0] + moveDir[1] * moveDir[1];
|
||||
if(moveDirMagSq < ENTITY_PLAYER_MOVE_ROTATE_THRESHOLD_SQ) return;
|
||||
|
||||
componentid_t posComp = entityGetComponent(
|
||||
mgr, entityId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
if(posComp == COMPONENT_ID_INVALID) return;
|
||||
|
||||
vec3 rotation;
|
||||
entityPositionGetLocalRotation(mgr, entityId, posComp, rotation);
|
||||
// The player's "front" is +local Z, not -Z (entityCameraGetForward's
|
||||
// convention is for cameras, which look down -Z -- the player model
|
||||
// faces the opposite way), which for a pure yaw rotation works out to
|
||||
// (sin(yaw), 0, cos(yaw)) -- invert that to solve for the yaw facing
|
||||
// moveDir.
|
||||
rotation[1] = atan2f(moveDir[0], moveDir[1]);
|
||||
entityPositionSetLocalRotation(mgr, entityId, posComp, rotation);
|
||||
}
|
||||
|
||||
errorret_t entityPlayerSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
) {
|
||||
entityplayer_t *player = entityPlayerGet(mgr, entityId, componentId);
|
||||
|
||||
yyjson_mut_obj_add_real(doc, json, "moveSpeed", player->moveSpeed);
|
||||
yyjson_mut_obj_add_real(doc, json, "jumpImpulse", player->jumpImpulse);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t entityPlayerDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_val *json
|
||||
) {
|
||||
entityplayer_t *player = entityPlayerGet(mgr, entityId, componentId);
|
||||
|
||||
yyjson_val *v;
|
||||
if((v = yyjson_obj_get(json, "moveSpeed"))) {
|
||||
player->moveSpeed = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
if((v = yyjson_obj_get(json, "jumpImpulse"))) {
|
||||
player->jumpImpulse = (float_t)yyjson_get_num(v);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#pragma once
|
||||
#include "entity/entitybase.h"
|
||||
#include "error/error.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
typedef struct {
|
||||
float_t moveSpeed;
|
||||
@@ -48,8 +50,14 @@ entityplayer_t *entityPlayerGet(
|
||||
* 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.
|
||||
* axes. No-op if the entity has no physics component.
|
||||
*
|
||||
* Also yaws the entity's position component (if any) to face the
|
||||
* direction of movement. Facing is only updated while there's actual
|
||||
* movement input -- releasing input leaves the entity facing whichever
|
||||
* way it last moved, rather than snapping to a default orientation.
|
||||
*
|
||||
* Registered automatically as an update callback by entityPlayerInit.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
@@ -62,3 +70,40 @@ void entityPlayerUpdate(
|
||||
const componentid_t componentId,
|
||||
void *user
|
||||
);
|
||||
|
||||
/**
|
||||
* Serializes the player's "moveSpeed" and "jumpImpulse" into the given
|
||||
* JSON object.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param doc The mutable JSON document to allocate values from.
|
||||
* @param json The JSON object to write the component's fields into.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entityPlayerSerialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *json
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads "moveSpeed" and "jumpImpulse" (see entityPlayerSerialize()) from
|
||||
* the given JSON object and applies whichever are present, leaving the
|
||||
* rest at their current values.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param json The JSON object to read the component's fields from.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t entityPlayerDeserialize(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
yyjson_val *json
|
||||
);
|
||||
|
||||
@@ -15,5 +15,8 @@
|
||||
// Init function (optional)
|
||||
// Dispose function (optional)
|
||||
// Render function (optional)
|
||||
// Serialize function (optional)
|
||||
// Deserialize function (optional)
|
||||
|
||||
X(PLAYER, entityplayer_t, player, entityPlayerInit, NULL, NULL)
|
||||
X(PLAYER, entityplayer_t, player, entityPlayerInit, NULL, NULL,
|
||||
entityPlayerSerialize, entityPlayerDeserialize)
|
||||
|
||||
+25
-1
@@ -8,9 +8,33 @@
|
||||
#include "game/game.h"
|
||||
#include "scene/scene.h"
|
||||
#include "scene/overworldscene.h"
|
||||
#include "assert/assert.h"
|
||||
#include "asset/asset.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define GAME_TEST_SCENE_ASSET "scenes/test.json"
|
||||
|
||||
errorret_t gameInit(void) {
|
||||
sceneSetActive(overworldSceneCreate());
|
||||
// overworldSceneCreate();
|
||||
|
||||
sceneid_t testSceneId = sceneCreate();
|
||||
|
||||
// Exercises sceneDeserialize() end-to-end: a camera looking at a
|
||||
// colored cube that falls under gravity onto a static ground plane,
|
||||
// described entirely as JSON rather than built with the entity API.
|
||||
assetentry_t *sceneEntry = assetLock(
|
||||
GAME_TEST_SCENE_ASSET, ASSET_LOADER_TYPE_JSON, NULL
|
||||
);
|
||||
errorret_t ret = assetRequireLoaded(sceneEntry);
|
||||
if(errorIsOk(ret)) {
|
||||
ret = sceneDeserialize(
|
||||
testSceneId, yyjson_doc_get_root(sceneEntry->data.json)
|
||||
);
|
||||
}
|
||||
assetUnlockEntry(sceneEntry);
|
||||
errorChain(ret);
|
||||
|
||||
sceneSetActive(testSceneId);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,32 @@ static void test_entityAddGetDisposeComponent(void **state) {
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_entitySetGetFindByName(void **state) {
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
|
||||
entityid_t a = entityManagerAdd(&mgr);
|
||||
entityid_t b = entityManagerAdd(&mgr);
|
||||
|
||||
assert_string_equal(entityGetName(&mgr, a), "");
|
||||
assert_int_equal(entityFindByName(&mgr, "camera"), ENTITY_ID_INVALID);
|
||||
|
||||
entitySetName(&mgr, a, "camera");
|
||||
assert_string_equal(entityGetName(&mgr, a), "camera");
|
||||
assert_int_equal(entityFindByName(&mgr, "camera"), a);
|
||||
assert_int_equal(entityFindByName(&mgr, "nonexistent"), ENTITY_ID_INVALID);
|
||||
|
||||
// Renaming doesn't leave the old name findable, and other entities stay
|
||||
// unaffected.
|
||||
entitySetName(&mgr, a, "player");
|
||||
assert_int_equal(entityFindByName(&mgr, "camera"), ENTITY_ID_INVALID);
|
||||
assert_int_equal(entityFindByName(&mgr, "player"), a);
|
||||
assert_string_equal(entityGetName(&mgr, b), "");
|
||||
|
||||
entityManagerDispose(&mgr);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_entityAddDuplicateComponentAsserts(void **state) {
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
@@ -90,6 +116,7 @@ int main(int argc, char **argv) {
|
||||
cmocka_unit_test(test_entityManagerAddIsolatesEntities),
|
||||
cmocka_unit_test(test_entityManagerFullAsserts),
|
||||
cmocka_unit_test(test_entityAddGetDisposeComponent),
|
||||
cmocka_unit_test(test_entitySetGetFindByName),
|
||||
cmocka_unit_test(test_entityAddDuplicateComponentAsserts),
|
||||
};
|
||||
|
||||
|
||||
@@ -94,6 +94,50 @@ static void test_entityPositionParentChildWorldTransform(void **state) {
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_entityPositionDeserializeLookAt(void **state) {
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
|
||||
entityid_t entity = entityManagerAdd(&mgr);
|
||||
componentid_t comp = entityAddComponent(
|
||||
&mgr, entity, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
|
||||
const char_t *json =
|
||||
"{ \"x\": 0, \"y\": 5, \"z\": 10, "
|
||||
"\"lookAt\": { \"x\": 0, \"y\": 0, \"z\": 0 } }";
|
||||
yyjson_doc *doc = yyjson_read(json, strlen(json), YYJSON_READ_NOFLAG);
|
||||
assert_non_null(doc);
|
||||
|
||||
errorret_t ret = entityPositionDeserialize(
|
||||
&mgr, entity, comp, yyjson_doc_get_root(doc)
|
||||
);
|
||||
yyjson_doc_free(doc);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
// The eye point decoded back out of localTransform should match "x"/"y"/"z".
|
||||
vec3 eye;
|
||||
entityPositionGetLocalPosition(&mgr, entity, comp, eye);
|
||||
assert_float_equal(eye[0], 0.0f, 0.0001f);
|
||||
assert_float_equal(eye[1], 5.0f, 0.0001f);
|
||||
assert_float_equal(eye[2], 10.0f, 0.0001f);
|
||||
|
||||
// Forward (-local Z, see entityCameraGetForward's convention) should
|
||||
// point from eye toward the lookAt target (the origin here).
|
||||
mat4 transform;
|
||||
entityPositionGetLocalTransform(&mgr, entity, comp, transform);
|
||||
vec3 forward = { -transform[2][0], -transform[2][1], -transform[2][2] };
|
||||
glm_vec3_normalize(forward);
|
||||
|
||||
vec3 toTarget = { -eye[0], -eye[1], -eye[2] };
|
||||
glm_vec3_normalize(toTarget);
|
||||
|
||||
assert_float_equal(glm_vec3_dot(forward, toTarget), 1.0f, 0.001f);
|
||||
|
||||
entityManagerDispose(&mgr);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_entityPositionDisposeDeep(void **state) {
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
@@ -123,6 +167,7 @@ int main(int argc, char **argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_entityPositionLocalGetSet),
|
||||
cmocka_unit_test(test_entityPositionParentChildWorldTransform),
|
||||
cmocka_unit_test(test_entityPositionDeserializeLookAt),
|
||||
cmocka_unit_test(test_entityPositionDisposeDeep),
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "time/time.h"
|
||||
#include "scene/scene.h"
|
||||
#include "entity/entitymanager.h"
|
||||
#include "entity/component/display/entityposition.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
static void countingUpdateCallback(
|
||||
entitymanager_t *mgr,
|
||||
@@ -137,6 +139,82 @@ static void test_sceneUpdateOnlyRunsFixedUpdateOnNonDynamicFrames(void **state)
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_sceneSerializeDeserializeParentLink(void **state) {
|
||||
sceneInit();
|
||||
|
||||
sceneid_t sceneA = sceneCreate();
|
||||
entitymanager_t *mgrA = sceneGetEntities(sceneA);
|
||||
|
||||
entityid_t parent = entityManagerAdd(mgrA);
|
||||
componentid_t parentPos = entityAddComponent(
|
||||
mgrA, parent, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
entitySetName(mgrA, parent, "root");
|
||||
entityPositionSetLocalPosition(mgrA, parent, parentPos, (vec3){ 10, 0, 0 });
|
||||
|
||||
entityid_t child = entityManagerAdd(mgrA);
|
||||
componentid_t childPos = entityAddComponent(
|
||||
mgrA, child, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
entityPositionSetLocalPosition(mgrA, child, childPos, (vec3){ 1, 0, 0 });
|
||||
entityPositionSetParent(mgrA, child, childPos, parent, parentPos);
|
||||
|
||||
vec3 childWorldBefore;
|
||||
entityPositionGetWorldPosition(mgrA, child, childPos, childWorldBefore);
|
||||
assert_float_equal(childWorldBefore[0], 11.0f, 0.0001f);
|
||||
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
yyjson_mut_val *root = yyjson_mut_obj(doc);
|
||||
yyjson_mut_doc_set_root(doc, root);
|
||||
errorret_t ret = sceneSerialize(sceneA, doc, root);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
size_t len = 0;
|
||||
char_t *jsonStr = yyjson_mut_write(doc, 0, &len);
|
||||
assert_non_null(jsonStr);
|
||||
yyjson_mut_doc_free(doc);
|
||||
|
||||
yyjson_doc *readDoc = yyjson_read(jsonStr, len, 0);
|
||||
free(jsonStr);
|
||||
assert_non_null(readDoc);
|
||||
|
||||
sceneid_t sceneB = sceneCreate();
|
||||
entitymanager_t *mgrB = sceneGetEntities(sceneB);
|
||||
ret = sceneDeserialize(sceneB, yyjson_doc_get_root(readDoc));
|
||||
yyjson_doc_free(readDoc);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
entityid_t reloadedParent = entityFindByName(mgrB, "root");
|
||||
assert_true(reloadedParent != ENTITY_ID_INVALID);
|
||||
|
||||
entityid_t reloadedChild = ENTITY_ID_INVALID;
|
||||
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||
if(!(mgrB->entities[i].state & ENTITY_STATE_ACTIVE)) continue;
|
||||
if(i == reloadedParent) continue;
|
||||
reloadedChild = i;
|
||||
break;
|
||||
}
|
||||
assert_true(reloadedChild != ENTITY_ID_INVALID);
|
||||
|
||||
componentid_t reloadedChildPos = entityGetComponent(
|
||||
mgrB, reloadedChild, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
assert_true(reloadedChildPos != COMPONENT_ID_INVALID);
|
||||
assert_int_equal(
|
||||
entityPositionGetParent(mgrB, reloadedChild, reloadedChildPos),
|
||||
reloadedParent
|
||||
);
|
||||
|
||||
vec3 childWorldAfter;
|
||||
entityPositionGetWorldPosition(
|
||||
mgrB, reloadedChild, reloadedChildPos, childWorldAfter
|
||||
);
|
||||
assert_float_equal(childWorldAfter[0], childWorldBefore[0], 0.0001f);
|
||||
|
||||
sceneDispose();
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_sceneDestroyClearsActive(void **state) {
|
||||
sceneInit();
|
||||
|
||||
@@ -157,6 +235,7 @@ int main(int argc, char **argv) {
|
||||
cmocka_unit_test(test_sceneEntitiesAreIsolatedPerScene),
|
||||
cmocka_unit_test(test_sceneFixedUpdateOnlyTicksActiveScene),
|
||||
cmocka_unit_test(test_sceneUpdateOnlyRunsFixedUpdateOnNonDynamicFrames),
|
||||
cmocka_unit_test(test_sceneSerializeDeserializeParentLink),
|
||||
cmocka_unit_test(test_sceneDestroyClearsActive),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user