Checkpoint 1: implement native subsystems and begin the gameplay manual
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
#include "Physics.hpp"
|
||||
#include <box2d/box2d.h>
|
||||
#include <box3d/box3d.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace faset::runtime::detail {
|
||||
namespace {
|
||||
b3Quat quaternion(Vec3 e) {
|
||||
auto x = b3MakeQuatFromAxisAngle({1, 0, 0}, e[0]);
|
||||
auto y = b3MakeQuatFromAxisAngle({0, 1, 0}, e[1]);
|
||||
auto z = b3MakeQuatFromAxisAngle({0, 0, 1}, e[2]);
|
||||
return b3MulQuat(z, b3MulQuat(y, x));
|
||||
}
|
||||
Vec3 euler(b3Quat q) {
|
||||
const float x=q.v.x, y=q.v.y, z=q.v.z, w=q.s;
|
||||
return {std::atan2(2*(w*x+y*z), 1-2*(x*x+y*y)),
|
||||
std::asin(std::clamp(2*(w*y-z*x), -1.0f, 1.0f)),
|
||||
std::atan2(2*(w*z+x*y), 1-2*(y*y+z*z))};
|
||||
}
|
||||
}
|
||||
struct Physics::Impl {
|
||||
struct Body { b2BodyId two{}; b3BodyId three{}; std::uint64_t shape{}; bool dynamic{}; };
|
||||
int dimension;
|
||||
int substeps;
|
||||
b2WorldId world2{};
|
||||
b3WorldId world3{};
|
||||
std::unordered_map<std::uint32_t, Body> bodies;
|
||||
std::unordered_map<std::uint64_t, std::uint32_t> shapes;
|
||||
Impl(int dim, Vec3 gravity, int count):dimension(dim),substeps(count) {
|
||||
if(dim==2) { auto def=b2DefaultWorldDef(); def.gravity={gravity[0],gravity[1]}; world2=b2CreateWorld(&def); }
|
||||
else { auto def=b3DefaultWorldDef(); def.gravity={gravity[0],gravity[1],gravity[2]}; world3=b3CreateWorld(&def); }
|
||||
}
|
||||
~Impl() { if(dimension==2) b2DestroyWorld(world2); else b3DestroyWorld(world3); }
|
||||
};
|
||||
Physics::Physics(int dimension, Vec3 gravity, int substeps):impl_(std::make_unique<Impl>(dimension,gravity,substeps)){}
|
||||
Physics::~Physics()=default;
|
||||
void Physics::add(std::uint32_t id, const Transform& t, const BodySettings& settings) {
|
||||
if(contains(id)) throw std::logic_error("physics body already exists");
|
||||
Impl::Body body{}; body.dynamic=settings.type=="dynamic";
|
||||
if(impl_->dimension==2) {
|
||||
auto def=b2DefaultBodyDef();
|
||||
def.type=settings.type=="static"?b2_staticBody:settings.type=="kinematic"?b2_kinematicBody:b2_dynamicBody;
|
||||
def.position={t.position[0],t.position[1]}; def.rotation=b2MakeRot(t.rotation[2]);
|
||||
def.linearVelocity={settings.linearVelocity[0],settings.linearVelocity[1]}; def.gravityScale=settings.gravityScale;
|
||||
body.two=b2CreateBody(impl_->world2,&def);
|
||||
auto shape=b2DefaultShapeDef(); shape.density=settings.density; shape.material.friction=settings.friction;
|
||||
shape.material.restitution=settings.restitution; shape.enableContactEvents=true;
|
||||
shape.filter.categoryBits=settings.categoryBits; shape.filter.maskBits=settings.maskBits;
|
||||
const auto box=b2MakeBox(settings.halfExtents[0]*std::abs(t.scale[0]),settings.halfExtents[1]*std::abs(t.scale[1]));
|
||||
body.shape=b2StoreShapeId(b2CreatePolygonShape(body.two,&shape,&box));
|
||||
} else {
|
||||
auto def=b3DefaultBodyDef();
|
||||
def.type=settings.type=="static"?b3_staticBody:settings.type=="kinematic"?b3_kinematicBody:b3_dynamicBody;
|
||||
def.position={t.position[0],t.position[1],t.position[2]}; def.rotation=quaternion(t.rotation);
|
||||
def.linearVelocity={settings.linearVelocity[0],settings.linearVelocity[1],settings.linearVelocity[2]}; def.gravityScale=settings.gravityScale;
|
||||
body.three=b3CreateBody(impl_->world3,&def);
|
||||
auto shape=b3DefaultShapeDef(); shape.density=settings.density; shape.baseMaterial.friction=settings.friction;
|
||||
shape.baseMaterial.restitution=settings.restitution; shape.enableContactEvents=true;
|
||||
shape.filter.categoryBits=settings.categoryBits; shape.filter.maskBits=settings.maskBits;
|
||||
auto box=b3MakeBoxHull(settings.halfExtents[0]*std::abs(t.scale[0]),settings.halfExtents[1]*std::abs(t.scale[1]),settings.halfExtents[2]*std::abs(t.scale[2]));
|
||||
body.shape=b3StoreShapeId(b3CreateHullShape(body.three,&shape,&box.base));
|
||||
}
|
||||
impl_->shapes.emplace(body.shape,id); impl_->bodies.emplace(id,body);
|
||||
}
|
||||
void Physics::remove(std::uint32_t id) {
|
||||
const auto it=impl_->bodies.find(id); if(it==impl_->bodies.end()) return;
|
||||
impl_->shapes.erase(it->second.shape);
|
||||
if(impl_->dimension==2) b2DestroyBody(it->second.two); else b3DestroyBody(it->second.three);
|
||||
impl_->bodies.erase(it);
|
||||
}
|
||||
bool Physics::contains(std::uint32_t id) const { return impl_->bodies.contains(id); }
|
||||
bool Physics::dynamic(std::uint32_t id) const { return impl_->bodies.at(id).dynamic; }
|
||||
Transform Physics::transform(std::uint32_t id, Transform t) const {
|
||||
const auto& body=impl_->bodies.at(id);
|
||||
if(impl_->dimension==2) { auto p=b2Body_GetPosition(body.two); t.position[0]=p.x;t.position[1]=p.y;t.rotation[2]=b2Rot_GetAngle(b2Body_GetRotation(body.two)); }
|
||||
else { auto p=b3Body_GetPosition(body.three);t.position={float(p.x),float(p.y),float(p.z)};t.rotation=euler(b3Body_GetRotation(body.three)); }
|
||||
return t;
|
||||
}
|
||||
Vec3 Physics::velocity(std::uint32_t id) const {
|
||||
const auto& body=impl_->bodies.at(id);
|
||||
if(impl_->dimension==2) { auto v=b2Body_GetLinearVelocity(body.two);return {v.x,v.y,0}; }
|
||||
auto v=b3Body_GetLinearVelocity(body.three);return {v.x,v.y,v.z};
|
||||
}
|
||||
void Physics::teleport(std::uint32_t id, const Transform& t) {
|
||||
const auto& body=impl_->bodies.at(id);
|
||||
if(impl_->dimension==2) b2Body_SetTransform(body.two,{t.position[0],t.position[1]},b2MakeRot(t.rotation[2]));
|
||||
else b3Body_SetTransform(body.three,{t.position[0],t.position[1],t.position[2]},quaternion(t.rotation));
|
||||
}
|
||||
void Physics::setVelocity(std::uint32_t id, Vec3 v) {
|
||||
const auto& body=impl_->bodies.at(id);
|
||||
if(impl_->dimension==2) b2Body_SetLinearVelocity(body.two,{v[0],v[1]}); else b3Body_SetLinearVelocity(body.three,{v[0],v[1],v[2]});
|
||||
}
|
||||
void Physics::impulse(std::uint32_t id, Vec3 v) {
|
||||
const auto& body=impl_->bodies.at(id);
|
||||
if(impl_->dimension==2) b2Body_ApplyLinearImpulseToCenter(body.two,{v[0],v[1]},true); else b3Body_ApplyLinearImpulseToCenter(body.three,{v[0],v[1],v[2]},true);
|
||||
}
|
||||
std::vector<Contact> Physics::step(float delta) {
|
||||
std::vector<Contact> contacts;
|
||||
auto append=[&](std::uint64_t a,std::uint64_t b,bool began) {
|
||||
auto first=impl_->shapes.find(a),second=impl_->shapes.find(b);
|
||||
if(first!=impl_->shapes.end() && second!=impl_->shapes.end()) contacts.push_back({first->second,second->second,began});
|
||||
};
|
||||
if(impl_->dimension==2) {
|
||||
b2World_Step(impl_->world2,delta,impl_->substeps);auto events=b2World_GetContactEvents(impl_->world2);
|
||||
for(int i=0;i<events.beginCount;++i) append(b2StoreShapeId(events.beginEvents[i].shapeIdA),b2StoreShapeId(events.beginEvents[i].shapeIdB),true);
|
||||
for(int i=0;i<events.endCount;++i) append(b2StoreShapeId(events.endEvents[i].shapeIdA),b2StoreShapeId(events.endEvents[i].shapeIdB),false);
|
||||
} else {
|
||||
b3World_Step(impl_->world3,delta,impl_->substeps);auto events=b3World_GetContactEvents(impl_->world3);
|
||||
for(int i=0;i<events.beginCount;++i) append(b3StoreShapeId(events.beginEvents[i].shapeIdA),b3StoreShapeId(events.beginEvents[i].shapeIdB),true);
|
||||
for(int i=0;i<events.endCount;++i) append(b3StoreShapeId(events.endEvents[i].shapeIdA),b3StoreShapeId(events.endEvents[i].shapeIdB),false);
|
||||
}
|
||||
return contacts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
#include <faset/runtime/Runtime.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace faset::runtime::detail {
|
||||
struct BodySettings {
|
||||
std::string type{"dynamic"};
|
||||
Vec3 halfExtents{0.5f, 0.5f, 0.5f};
|
||||
Vec3 linearVelocity{};
|
||||
float density{1};
|
||||
float friction{0.3f};
|
||||
float restitution{};
|
||||
float gravityScale{1};
|
||||
std::uint64_t categoryBits{1};
|
||||
std::uint64_t maskBits{~std::uint64_t{0}};
|
||||
};
|
||||
struct Contact { std::uint32_t first; std::uint32_t second; bool began; };
|
||||
class Physics {
|
||||
public:
|
||||
Physics(int dimension, Vec3 gravity, int substeps);
|
||||
~Physics();
|
||||
void add(std::uint32_t id, const Transform&, const BodySettings&);
|
||||
void remove(std::uint32_t id);
|
||||
bool contains(std::uint32_t id) const;
|
||||
bool dynamic(std::uint32_t id) const;
|
||||
Transform transform(std::uint32_t id, Transform previous) const;
|
||||
Vec3 velocity(std::uint32_t id) const;
|
||||
void teleport(std::uint32_t id, const Transform&);
|
||||
void setVelocity(std::uint32_t id, Vec3);
|
||||
void impulse(std::uint32_t id, Vec3);
|
||||
std::vector<Contact> step(float delta);
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
#include <faset/runtime/Runtime.hpp>
|
||||
#include "Physics.hpp"
|
||||
#include <entt/entt.hpp>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <deque>
|
||||
#include <numbers>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace faset::runtime {
|
||||
namespace {
|
||||
using Json=nlohmann::json;
|
||||
std::atomic<std::uint64_t> nextSession{1};
|
||||
constexpr const char* body2="faset.rigid_body_2d";
|
||||
constexpr const char* body3="faset.rigid_body_3d";
|
||||
void require(bool condition,const std::string& message) { if(!condition) throw std::invalid_argument(message); }
|
||||
template<std::size_t N> std::array<float,N> vectorValue(const Json& object,const char* key,std::array<float,N> fallback) {
|
||||
if(!object.contains(key)) return fallback;
|
||||
const auto& value=object.at(key); require(value.is_array()&&value.size()==N,std::string(key)+": wrong vector size");
|
||||
for(std::size_t i=0;i<N;++i) { require(value[i].is_number(),std::string(key)+": expected number"); fallback[i]=value[i].get<float>(); require(std::isfinite(fallback[i]),std::string(key)+": nonfinite value"); }
|
||||
return fallback;
|
||||
}
|
||||
float number(const Json& fields,const char* key,float fallback) {
|
||||
if(!fields.contains(key)) return fallback;
|
||||
require(fields.at(key).is_number(),std::string(key)+": expected number");
|
||||
float v=fields.at(key).get<float>();require(std::isfinite(v),std::string(key)+": nonfinite value");return v;
|
||||
}
|
||||
Transform readTransform(const Json& fields) {
|
||||
return {vectorValue<3>(fields,"position",{0,0,0}),vectorValue<3>(fields,"rotation",{0,0,0}),vectorValue<3>(fields,"scale",{1,1,1})};
|
||||
}
|
||||
void validateTransform(const Transform& t) {
|
||||
for(const auto& values:{t.position,t.rotation,t.scale}) for(float value:values) require(std::isfinite(value),"nonfinite transform");
|
||||
}
|
||||
Json transformJson(const Transform& t) { return {{"position",t.position},{"rotation",t.rotation},{"scale",t.scale}}; }
|
||||
detail::BodySettings settings(const Json& fields,int dimension) {
|
||||
detail::BodySettings b;
|
||||
b.type=fields.value("body_type",std::string("dynamic"));require(b.type=="dynamic"||b.type=="static"||b.type=="kinematic","invalid body_type");
|
||||
if(dimension==2) {
|
||||
auto half=vectorValue<2>(fields,"half_extents",{0.5f,0.5f});b.halfExtents={half[0],half[1],0.5f};
|
||||
auto vel=vectorValue<2>(fields,"linear_velocity",{0,0});b.linearVelocity={vel[0],vel[1],0};
|
||||
} else { b.halfExtents=vectorValue<3>(fields,"half_extents",{0.5f,0.5f,0.5f});b.linearVelocity=vectorValue<3>(fields,"linear_velocity",{0,0,0}); }
|
||||
for(float extent:b.halfExtents) require(extent>0&&extent<100000,"half_extents must be positive and finite");
|
||||
b.density=number(fields,"density",1); b.friction=number(fields,"friction",0.3f);
|
||||
b.restitution=number(fields,"restitution",0);b.gravityScale=number(fields,"gravity_scale",1);
|
||||
require(b.density>0&&b.friction>=0&&b.restitution>=0&&b.restitution<=1,"invalid physics material");
|
||||
auto bits=[&](const char* name,std::uint64_t fallback) { if(!fields.contains(name))return fallback; const auto& value=fields.at(name);require(value.is_number_unsigned()||(value.is_number_integer()&&value.get<std::int64_t>()>=0),std::string(name)+": expected nonnegative bits");return value.get<std::uint64_t>(); };
|
||||
b.categoryBits=bits("category_bits",1);b.maskBits=bits("mask_bits",~std::uint64_t{0});return b;
|
||||
}
|
||||
void validateEntity(const Json& entity,int dimension) {
|
||||
require(entity.is_object(),"entity must be an object");
|
||||
require(entity.contains("id")&&entity["id"].is_string()&&!entity["id"].get<std::string>().empty(),"entity requires id");
|
||||
require(!entity.contains("name")||entity["name"].is_string(),"entity name must be a string");
|
||||
require(entity.contains("components")&&entity["components"].is_array(),"entity requires components array");
|
||||
if(entity.contains("parent"))require(entity["parent"].is_null()||entity["parent"].is_string(),"parent must be an id or null");
|
||||
std::set<std::string> types, ids;Transform transform{};bool physical=false;
|
||||
for(const auto& component:entity["components"]) {
|
||||
require(component.is_object()&&component.contains("id")&&component["id"].is_string()&&!component["id"].get<std::string>().empty(),"component requires id");
|
||||
require(component.contains("type")&&component["type"].is_string()&&!component["type"].get<std::string>().empty(),"component requires type");
|
||||
require(component.value("version",1)==1,"unsupported component version");
|
||||
require(component.contains("fields")&&component["fields"].is_object(),"component requires fields");
|
||||
require(ids.insert(component["id"].get<std::string>()).second,"duplicate component id");
|
||||
const auto type=component["type"].get<std::string>();require(types.insert(type).second,"duplicate component type");
|
||||
const auto& f=component["fields"];
|
||||
if(type=="faset.transform")transform=readTransform(f);
|
||||
if(type==body2||type==body3) { require(type==(dimension==2?body2:body3),"physics dimension does not match scene");settings(f,dimension);physical=true; }
|
||||
if(type=="faset.sprite") {vectorValue<4>(f,"color",{1,1,1,1});auto size=vectorValue<2>(f,"size",{1,1});require(size[0]>0&&size[1]>0,"sprite size must be positive");require(!f.contains("texture")||f["texture"].is_string(),"sprite texture must be a string");require(!f.contains("layer")||f["layer"].is_number_integer(),"sprite layer must be an integer");}
|
||||
if(type=="faset.mesh") {vectorValue<4>(f,"color",{1,1,1,1});require(!f.contains("asset")||f["asset"].is_string(),"mesh asset must be a string");require(!f.contains("primitive")||f["primitive"].is_string(),"mesh primitive must be a string");}
|
||||
}
|
||||
if(physical) {
|
||||
require(!entity.contains("parent")||entity["parent"].is_null(),"physics bodies must be root entities in the initial runtime");
|
||||
for(int i=0;i<dimension;++i)require(std::abs(transform.scale[i])>0.00001f,"physics scale must be nonzero");
|
||||
if(dimension==2) require(transform.rotation[0]==0&&transform.rotation[1]==0,"2D physics rotates only around Z");
|
||||
}
|
||||
}
|
||||
Transform interpolate(const Transform& a,const Transform& b,float alpha) {
|
||||
Transform out;
|
||||
for(int i=0;i<3;++i) {
|
||||
out.position[i]=std::lerp(a.position[i],b.position[i],alpha);out.scale[i]=std::lerp(a.scale[i],b.scale[i],alpha);
|
||||
}
|
||||
auto quaternion=[](Vec3 r) {
|
||||
const float cx=std::cos(r[0]*0.5f),sx=std::sin(r[0]*0.5f),cy=std::cos(r[1]*0.5f),sy=std::sin(r[1]*0.5f),cz=std::cos(r[2]*0.5f),sz=std::sin(r[2]*0.5f);
|
||||
return Vec4{sx*cy*cz-cx*sy*sz,cx*sy*cz+sx*cy*sz,cx*cy*sz-sx*sy*cz,cx*cy*cz+sx*sy*sz};
|
||||
};
|
||||
auto qa=quaternion(a.rotation),qb=quaternion(b.rotation);float dot=0;
|
||||
for(int i=0;i<4;++i)dot+=qa[i]*qb[i];
|
||||
if(dot<0){for(auto& q:qb)q=-q;dot=-dot;}
|
||||
float wa=1-alpha,wb=alpha;
|
||||
if(dot<0.9995f){const float angle=std::acos(std::clamp(dot,-1.0f,1.0f)),denom=std::sin(angle);wa=std::sin((1-alpha)*angle)/denom;wb=std::sin(alpha*angle)/denom;}
|
||||
Vec4 q{};float length=0;for(int i=0;i<4;++i){q[i]=wa*qa[i]+wb*qb[i];length+=q[i]*q[i];}for(auto& v:q)v/=std::sqrt(length);
|
||||
const auto [x,y,z,w]=q;
|
||||
out.rotation={std::atan2(2*(w*x+y*z),1-2*(x*x+y*y)),std::asin(std::clamp(2*(w*y-z*x),-1.0f,1.0f)),std::atan2(2*(w*z+x*y),1-2*(y*y+z*z))};
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
struct Runtime::Impl {
|
||||
struct Data { Json document; std::uint64_t generation; };
|
||||
struct Pose { Transform previous,current,presented; bool changedInUpdate{}; };
|
||||
enum class Phase { Idle, Initialize, Fixed, Update, Late, Destroy };
|
||||
enum class Kind { Spawn, Destroy, Add, Remove };
|
||||
struct Command { Kind kind; EntityHandle handle; Json payload; std::string type; };
|
||||
Runtime* owner;
|
||||
RuntimeConfig config;
|
||||
entt::registry registry;
|
||||
std::unordered_map<std::string,entt::entity> ids;
|
||||
std::unordered_map<std::string,Behavior> behaviors;
|
||||
std::vector<entt::entity> order;
|
||||
std::deque<Command> pending;
|
||||
std::unique_ptr<detail::Physics> physics;
|
||||
std::vector<CollisionEvent> contacts;
|
||||
std::vector<std::string> diagnostics;
|
||||
std::uint64_t session{nextSession.fetch_add(1)};
|
||||
std::uint64_t generation{},tick{};
|
||||
int dimension{3};
|
||||
double accumulator{},alpha{};
|
||||
bool paused{},busy{};
|
||||
InputState currentInput{},queuedInput{};
|
||||
Phase phase{Phase::Idle};
|
||||
Impl(Runtime* runtime,RuntimeConfig cfg):owner(runtime),config(cfg){}
|
||||
EntityHandle handle(entt::entity e) const {return {session,entt::to_integral(e),registry.get<Data>(e).generation};}
|
||||
bool valid(EntityHandle h)const noexcept { auto e=static_cast<entt::entity>(h.slot);return h.session==session&®istry.valid(e)&®istry.all_of<Data>(e)&®istry.get<Data>(e).generation==h.generation; }
|
||||
entt::entity entity(EntityHandle h)const {if(!valid(h))throw std::invalid_argument("stale or foreign runtime handle");return static_cast<entt::entity>(h.slot);}
|
||||
const Json* component(entt::entity e,const std::string& type)const {for(const auto& c:registry.get<Data>(e).document["components"])if(c["type"]==type)return &c;return nullptr;}
|
||||
void callback(const Behavior::Callback& fn,entt::entity e,double dt) {
|
||||
if(!fn)return;
|
||||
try {fn(*owner,handle(e),dt);}catch(const std::exception& ex){diagnostics.push_back("gameplay "+registry.get<Data>(e).document["id"].get<std::string>()+": "+ex.what());}catch(...){diagnostics.push_back("unknown gameplay exception");}
|
||||
}
|
||||
void lifecycle(entt::entity e,Behavior::Callback Behavior::* member,double dt) {
|
||||
const auto components=registry.get<Data>(e).document["components"];
|
||||
for(const auto& c:components) {auto it=behaviors.find(c["type"].get<std::string>());if(it!=behaviors.end())callback(it->second.*member,e,dt);}
|
||||
}
|
||||
void all(Behavior::Callback Behavior::* member,double dt) {for(auto e:order)if(registry.valid(e))lifecycle(e,member,dt);}
|
||||
void syncVisual(entt::entity e) {
|
||||
if(auto c=component(e,"faset.sprite")){const auto& f=(*c)["fields"];registry.emplace_or_replace<Sprite>(e,vectorValue<4>(f,"color",{1,1,1,1}),vectorValue<2>(f,"size",{1,1}),f.value("texture",std::string{}),f.value("layer",0));}else registry.remove<Sprite>(e);
|
||||
if(auto c=component(e,"faset.mesh")){const auto& f=(*c)["fields"];registry.emplace_or_replace<Mesh>(e,f.value("asset",std::string{}),vectorValue<4>(f,"color",{1,1,1,1}),f.value("primitive",std::string("cube")));}else registry.remove<Mesh>(e);
|
||||
}
|
||||
void addPhysics(entt::entity e) {
|
||||
require(bool(physics),"load a scene before creating physics");
|
||||
auto c=component(e,dimension==2?body2:body3);if(c)physics->add(entt::to_integral(e),registry.get<Pose>(e).current,settings((*c)["fields"],dimension));
|
||||
}
|
||||
entt::entity create(Json document) {
|
||||
const auto id=document["id"].get<std::string>();require(!ids.contains(id),"duplicate entity id: "+id);
|
||||
auto e=registry.create();Transform t{};
|
||||
for(const auto& c:document["components"])if(c["type"]=="faset.transform")t=readTransform(c["fields"]);
|
||||
registry.emplace<Data>(e,std::move(document),++generation);registry.emplace<Pose>(e,t,t,t,false);ids.emplace(id,e);order.push_back(e);addPhysics(e);syncVisual(e);return e;
|
||||
}
|
||||
void erase(entt::entity e) {
|
||||
// Authoring hierarchy destruction has the same subtree semantics in runtime.
|
||||
auto id=registry.get<Data>(e).document["id"].get<std::string>();
|
||||
std::vector<entt::entity> children;
|
||||
for(auto child:order)if(registry.valid(child)&®istry.get<Data>(child).document.value("parent",Json{})==id)children.push_back(child);
|
||||
for(auto child:children)erase(child);
|
||||
phase=Phase::Destroy;lifecycle(e,&Behavior::onDestroy,0);
|
||||
physics->remove(entt::to_integral(e));ids.erase(id);registry.destroy(e);
|
||||
std::erase(order,e);
|
||||
}
|
||||
void commands() {
|
||||
auto commands=std::move(pending);pending.clear();
|
||||
for(auto& command:commands)try {
|
||||
if(command.kind==Kind::Spawn) {
|
||||
validateEntity(command.payload,dimension);
|
||||
auto parent=command.payload.value("parent",Json{});require(parent.is_null()||ids.contains(parent.get<std::string>()),"spawn parent is absent");
|
||||
auto e=create(std::move(command.payload));phase=Phase::Initialize;lifecycle(e,&Behavior::onStart,0);continue;
|
||||
}
|
||||
if(!valid(command.handle)){diagnostics.push_back("ignored structural command for stale handle");continue;}
|
||||
auto e=entity(command.handle);
|
||||
if(command.kind==Kind::Destroy){erase(e);continue;}
|
||||
auto candidate=registry.get<Data>(e).document;
|
||||
auto& components=candidate["components"];
|
||||
if(command.kind==Kind::Add) {components.push_back(command.payload);validateEntity(candidate,dimension);}
|
||||
else { auto it=std::find_if(components.begin(),components.end(),[&](const Json& c){return c["type"]==command.type;});if(it==components.end())continue;
|
||||
if(command.type=="faset.transform"&&physics->contains(command.handle.slot))throw std::invalid_argument("remove physics before removing transform");
|
||||
auto behavior=behaviors.find(command.type);if(behavior!=behaviors.end()){phase=Phase::Destroy;callback(behavior->second.onDestroy,e,0);}components.erase(it);
|
||||
}
|
||||
const std::string changed=command.kind==Kind::Add?command.payload["type"].get<std::string>():command.type;
|
||||
if((changed==body2||changed==body3)&&command.kind==Kind::Add) {
|
||||
const auto& pose=registry.get<Pose>(e).current;
|
||||
for(int i=0;i<dimension;++i)require(std::abs(pose.scale[i])>0.00001f,"runtime physics scale must be nonzero");
|
||||
if(dimension==2)require(pose.rotation[0]==0&&pose.rotation[1]==0,"2D physics rotates only around Z");
|
||||
}
|
||||
registry.get<Data>(e).document=std::move(candidate);syncVisual(e);
|
||||
if(changed==body2||changed==body3) {if(command.kind==Kind::Add)addPhysics(e);else physics->remove(command.handle.slot);}
|
||||
if(changed=="faset.transform") {auto& d=registry.get<Pose>(e);d.current=command.kind==Kind::Add?readTransform(command.payload["fields"]):Transform{};d.previous=d.presented=d.current;}
|
||||
if(command.kind==Kind::Add){auto it=behaviors.find(changed);if(it!=behaviors.end()){phase=Phase::Initialize;callback(it->second.onStart,e,0);}}
|
||||
}catch(const std::exception& ex){diagnostics.push_back(std::string("structural command rejected: ")+ex.what());}
|
||||
}
|
||||
void fixed() {
|
||||
commands();phase=Phase::Fixed;
|
||||
for(auto [e,d]:registry.view<Pose>().each()){(void)e;d.previous=d.current;}
|
||||
currentInput=queuedInput;queuedInput.jumpPressed=false;queuedInput.interactPressed=false;
|
||||
all(&Behavior::fixedUpdate,config.fixedDelta);
|
||||
contacts.clear();
|
||||
if(physics) {
|
||||
auto events=physics->step(static_cast<float>(config.fixedDelta));
|
||||
for(auto e:order)if(physics->contains(entt::to_integral(e))) {auto& d=registry.get<Pose>(e);d.current=physics->transform(entt::to_integral(e),d.current);}
|
||||
for(const auto& event:events) {
|
||||
auto a=static_cast<entt::entity>(event.first),b=static_cast<entt::entity>(event.second);
|
||||
if(!registry.valid(a)||!registry.valid(b))continue;
|
||||
contacts.push_back({handle(a),handle(b),event.began});
|
||||
}
|
||||
for(const auto& event:contacts)for(auto h:{event.first,event.second}) {
|
||||
auto e=entity(h);for(const auto& c:registry.get<Data>(e).document["components"]) {
|
||||
auto it=behaviors.find(c["type"].get<std::string>());if(it!=behaviors.end()&&it->second.onCollision)
|
||||
try{it->second.onCollision(*owner,h,event);}catch(const std::exception& ex){diagnostics.push_back(std::string("collision callback: ")+ex.what());}catch(...){diagnostics.push_back("unknown collision callback exception");}
|
||||
}
|
||||
}
|
||||
}
|
||||
++tick;phase=Phase::Idle;
|
||||
}
|
||||
FrameStats frame(double elapsed,InputState input,bool step) {
|
||||
require(std::isfinite(elapsed)&&elapsed>=0,"elapsed time must be finite and nonnegative");require(!busy,"recursive runtime advance");
|
||||
require(std::isfinite(input.horizontal)&&std::isfinite(input.vertical),"input axes must be finite");
|
||||
struct Guard {bool& busy;~Guard(){busy=false;}}guard{busy};busy=true;
|
||||
FrameStats stats{};
|
||||
if(paused&&!step){accumulator=0;currentInput={};queuedInput={};return {0,0,alpha,tick};}
|
||||
queuedInput.horizontal=input.horizontal;queuedInput.vertical=input.vertical;
|
||||
queuedInput.jumpPressed=queuedInput.jumpPressed||input.jumpPressed;queuedInput.interactPressed=queuedInput.interactPressed||input.interactPressed;
|
||||
for(auto [e,d]:registry.view<Pose>().each()){(void)e;d.changedInUpdate=false;}
|
||||
accumulator+=step?config.fixedDelta:elapsed;
|
||||
while(accumulator+1e-12>=config.fixedDelta&&stats.fixedTicks<(step?1u:config.maxCatchUpTicks)) {fixed();accumulator=std::max(0.0,accumulator-config.fixedDelta);++stats.fixedTicks;}
|
||||
if(accumulator>=config.fixedDelta){auto remaining=std::fmod(accumulator,config.fixedDelta);stats.droppedTime=accumulator-remaining;accumulator=remaining;diagnostics.push_back("dropped_time="+std::to_string(stats.droppedTime));}
|
||||
currentInput=input;phase=Phase::Update;all(&Behavior::update,step?config.fixedDelta:elapsed);
|
||||
alpha=step?1.0:std::clamp(accumulator/config.fixedDelta,0.0,1.0);
|
||||
for(auto [e,d]:registry.view<Pose>().each()){(void)e;d.presented=d.changedInUpdate?d.current:interpolate(d.previous,d.current,static_cast<float>(alpha));}
|
||||
phase=Phase::Late;all(&Behavior::lateUpdate,step?config.fixedDelta:elapsed);phase=Phase::Idle;
|
||||
stats.interpolationAlpha=alpha;stats.tick=tick;return stats;
|
||||
}
|
||||
};
|
||||
|
||||
Runtime::Runtime(RuntimeConfig cfg):impl_(std::make_unique<Impl>(this,cfg)) {
|
||||
require(std::isfinite(cfg.fixedDelta)&&cfg.fixedDelta>0&&cfg.fixedDelta<=1,"invalid fixed delta");
|
||||
require(cfg.maxCatchUpTicks>0&&cfg.maxCatchUpTicks<=1024,"invalid catchup limit");require(cfg.physicsSubsteps>0&&cfg.physicsSubsteps<=128,"invalid physics substeps");
|
||||
for(float value:cfg.gravity)require(std::isfinite(value),"invalid gravity");
|
||||
}
|
||||
Runtime::~Runtime(){try{clear();}catch(...){}}
|
||||
void Runtime::registerBehavior(std::string type,Behavior behavior) {
|
||||
require(!impl_->busy&&impl_->ids.empty(),"register gameplay before loading scene");require(!type.empty()&&!impl_->behaviors.contains(type),"duplicate or empty behavior type");impl_->behaviors.emplace(std::move(type),std::move(behavior));
|
||||
}
|
||||
void Runtime::load(const Json& scene) {
|
||||
require(!impl_->busy,"cannot load scene from gameplay callback");
|
||||
require(scene.is_object()&&scene.value("format",std::string{})=="faset.scene"&&scene.value("version",0)==1,"unsupported scene format/version");
|
||||
const int dimension=scene.value("dimension",3);require(dimension==2||dimension==3,"scene dimension must be 2 or 3");
|
||||
require(scene.contains("entities")&&scene["entities"].is_array(),"scene entities must be an array");
|
||||
require(!scene.contains("instances")||(scene["instances"].is_array()&&scene["instances"].empty()),"resolve template instances before runtime loading");
|
||||
std::unordered_map<std::string,Json> entities;
|
||||
for(const auto& entity:scene["entities"]) {validateEntity(entity,dimension);require(entities.emplace(entity["id"].get<std::string>(),entity).second,"duplicate scene entity id");}
|
||||
for(const auto& [id,entity]:entities) {
|
||||
std::set<std::string> visited{id};auto parent=entity.value("parent",Json{});
|
||||
while(!parent.is_null()){auto key=parent.get<std::string>();require(entities.contains(key),"unknown parent entity");require(visited.insert(key).second,"cyclic parent hierarchy");parent=entities.at(key).value("parent",Json{});}
|
||||
}
|
||||
auto next=std::make_unique<Impl>(this,impl_->config);next->dimension=dimension;next->behaviors=impl_->behaviors;
|
||||
next->physics=std::make_unique<detail::Physics>(dimension,next->config.gravity,next->config.physicsSubsteps);
|
||||
for(const auto& entity:scene["entities"])next->create(entity);
|
||||
clear();impl_=std::move(next);impl_->busy=true;impl_->phase=Impl::Phase::Initialize;impl_->all(&Behavior::onStart,0);impl_->phase=Impl::Phase::Idle;impl_->busy=false;
|
||||
}
|
||||
void Runtime::clear(){require(!impl_->busy,"cannot clear runtime from gameplay callback");impl_->busy=true;while(!impl_->order.empty())impl_->erase(impl_->order.back());impl_->pending.clear();impl_->contacts.clear();impl_->physics.reset();impl_->accumulator=0;impl_->tick=0;impl_->session=nextSession.fetch_add(1);impl_->busy=false;}
|
||||
FrameStats Runtime::advance(double dt,InputState input){return impl_->frame(dt,input,false);}
|
||||
FrameStats Runtime::singleStep(InputState input){return impl_->frame(0,input,true);}
|
||||
void Runtime::setPaused(bool value){require(!impl_->busy,"pause control belongs outside gameplay callbacks");impl_->paused=value;impl_->accumulator=0;impl_->queuedInput={};impl_->alpha=0;for(auto [e,pose]:impl_->registry.view<Impl::Pose>().each()){(void)e;pose.previous=pose.presented=pose.current;}}
|
||||
bool Runtime::paused()const noexcept{return impl_->paused;}
|
||||
EntityHandle Runtime::find(const std::string& id)const{auto it=impl_->ids.find(id);return it==impl_->ids.end()?EntityHandle{}:impl_->handle(it->second);}
|
||||
bool Runtime::valid(EntityHandle handle)const noexcept{return impl_->valid(handle);}
|
||||
Transform Runtime::transform(EntityHandle h)const{return impl_->registry.get<Impl::Pose>(impl_->entity(h)).current;}
|
||||
Transform Runtime::presentation(EntityHandle h)const{return impl_->registry.get<Impl::Pose>(impl_->entity(h)).presented;}
|
||||
Json Runtime::fields(EntityHandle h,const std::string& type)const{auto c=impl_->component(impl_->entity(h),type);if(!c)throw std::invalid_argument("entity has no component: "+type);return (*c)["fields"];}
|
||||
Vec3 Runtime::velocity(EntityHandle h)const{impl_->entity(h);if(!impl_->physics||!impl_->physics->contains(h.slot))throw std::invalid_argument("entity has no physics body");return impl_->physics->velocity(h.slot);}
|
||||
InputState Runtime::input()const noexcept{return impl_->currentInput;}
|
||||
const std::vector<CollisionEvent>& Runtime::collisions()const noexcept{return impl_->contacts;}
|
||||
void Runtime::setTransform(EntityHandle h,const Transform& value){validateTransform(value);auto e=impl_->entity(h);if(impl_->physics&&impl_->physics->contains(h.slot))throw std::invalid_argument("physics transform requires teleport");auto& d=impl_->registry.get<Impl::Pose>(e);d.current=value;if(impl_->phase!=Impl::Phase::Fixed){d.previous=d.presented=value;d.changedInUpdate=true;}}
|
||||
void Runtime::setPresentation(EntityHandle h,const Transform& value){validateTransform(value);require(impl_->phase==Impl::Phase::Late,"presentation may only be changed during LateUpdate");impl_->registry.get<Impl::Pose>(impl_->entity(h)).presented=value;}
|
||||
void Runtime::teleport(EntityHandle h,const Transform& value){validateTransform(value);auto e=impl_->entity(h);auto& d=impl_->registry.get<Impl::Pose>(e);if(impl_->physics&&impl_->physics->contains(h.slot)){require(d.current.scale==value.scale,"changing collider scale requires remove/add body");if(impl_->dimension==2)require(value.rotation[0]==0&&value.rotation[1]==0,"2D physics rotates only around Z");impl_->physics->teleport(h.slot,value);}d.previous=d.current=d.presented=value;d.changedInUpdate=true;}
|
||||
void Runtime::setVelocity(EntityHandle h,Vec3 value){impl_->entity(h);for(float v:value)require(std::isfinite(v),"nonfinite velocity");if(!impl_->physics||!impl_->physics->contains(h.slot))throw std::invalid_argument("entity has no physics body");impl_->physics->setVelocity(h.slot,value);}
|
||||
void Runtime::applyImpulse(EntityHandle h,Vec3 value){impl_->entity(h);for(float v:value)require(std::isfinite(v),"nonfinite impulse");if(!impl_->physics||!impl_->physics->contains(h.slot))throw std::invalid_argument("entity has no physics body");impl_->physics->impulse(h.slot,value);}
|
||||
void Runtime::spawn(Json entity){require(bool(impl_->physics),"load a scene before spawning");validateEntity(entity,impl_->dimension);impl_->pending.push_back({Impl::Kind::Spawn,{},std::move(entity),{}});}
|
||||
void Runtime::destroy(EntityHandle h){impl_->entity(h);impl_->pending.push_back({Impl::Kind::Destroy,h,{},{}});}
|
||||
void Runtime::addComponent(EntityHandle h,Json component){impl_->entity(h);impl_->pending.push_back({Impl::Kind::Add,h,std::move(component),{}});}
|
||||
void Runtime::removeComponent(EntityHandle h,const std::string& type){impl_->entity(h);impl_->pending.push_back({Impl::Kind::Remove,h,{},type});}
|
||||
RuntimeSnapshot Runtime::snapshot()const {
|
||||
RuntimeSnapshot out{impl_->dimension,impl_->tick,impl_->alpha,{}};out.entities.reserve(impl_->order.size());
|
||||
for(auto e:impl_->order) {const auto& d=impl_->registry.get<Impl::Data>(e);RenderEntity item;item.id=d.document["id"].get<std::string>();item.name=d.document.value("name",item.id);item.transform=impl_->registry.get<Impl::Pose>(e).presented;
|
||||
if(d.document.contains("parent")&&!d.document["parent"].is_null())item.parent=d.document["parent"].get<std::string>();
|
||||
if(auto sprite=impl_->registry.try_get<Sprite>(e))item.sprite=*sprite;
|
||||
if(auto mesh=impl_->registry.try_get<Mesh>(e))item.mesh=*mesh;
|
||||
out.entities.push_back(std::move(item));
|
||||
}return out;
|
||||
}
|
||||
Json Runtime::snapshotJson()const{auto value=snapshot();Json entities=Json::array();for(const auto& e:value.entities){Json item{{"id",e.id},{"name",e.name},{"parent",e.parent?Json(*e.parent):Json{}},{"transform",transformJson(e.transform)}};if(e.sprite)item["sprite"]={{"color",e.sprite->color},{"size",e.sprite->size},{"texture",e.sprite->texture},{"layer",e.sprite->layer}};if(e.mesh)item["mesh"]={{"asset",e.mesh->asset},{"color",e.mesh->color},{"primitive",e.mesh->primitive}};const auto entity=impl_->ids.at(e.id);for(const auto& type:{"faset.camera","faset.light"})if(auto c=impl_->component(entity,type))item[type==std::string("faset.camera")?"camera":"light"]=(*c)["fields"];entities.push_back(std::move(item));}return {{"dimension",value.dimension},{"tick",value.tick},{"alpha",value.alpha},{"entities",entities}};}
|
||||
std::uint64_t Runtime::session()const noexcept{return impl_->session;}
|
||||
const std::vector<std::string>& Runtime::diagnostics()const noexcept{return impl_->diagnostics;}
|
||||
}
|
||||
Reference in New Issue
Block a user