Checkpoint 2: integrate native Editor, MCP, gameplay builds and standalone export
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <faset/runtime/Runtime.hpp>
|
||||
|
||||
namespace beacon {
|
||||
inline nlohmann::json schema() {
|
||||
return {{"id", "example.beacon"},
|
||||
{"name", "Beacon"},
|
||||
{"version", 1},
|
||||
{"fields",
|
||||
{{"speed",
|
||||
{{"id", "speed"},
|
||||
{"name", "Rotation speed"},
|
||||
{"type", "number"},
|
||||
{"default", 1.0},
|
||||
{"units", "rad/s"}}}}}};
|
||||
}
|
||||
inline void register_behavior(faset::runtime::Runtime& runtime) {
|
||||
faset::runtime::Behavior behavior;
|
||||
behavior.fixedUpdate = [](faset::runtime::Runtime& world, faset::runtime::EntityHandle self,
|
||||
double dt) {
|
||||
auto pose = world.transform(self);
|
||||
pose.rotation[1] +=
|
||||
world.fields(self, "example.beacon").value("speed", 1.0f) * static_cast<float>(dt);
|
||||
world.setTransform(self, pose);
|
||||
};
|
||||
runtime.registerBehavior("example.beacon", std::move(behavior));
|
||||
}
|
||||
} // namespace beacon
|
||||
@@ -0,0 +1,119 @@
|
||||
#include <array>
|
||||
#include <faset/editor/plugin_api.h>
|
||||
#include <iomanip>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
using Json = nlohmann::json;
|
||||
struct State {
|
||||
const FasetEditorHost* host;
|
||||
};
|
||||
void collect(void* target, const char* data, uint64_t size) {
|
||||
static_cast<std::string*>(target)->append(data, size);
|
||||
}
|
||||
Json invoke(State& state, const char* command, const Json& arguments) {
|
||||
std::string output;
|
||||
const auto input = arguments.dump();
|
||||
const auto status =
|
||||
state.host->invoke_command(state.host->context, command, input.c_str(), collect, &output);
|
||||
auto result = Json::parse(output);
|
||||
if (status != 0)
|
||||
throw std::runtime_error(result.value("message", std::string("Editor command failed")));
|
||||
return result;
|
||||
}
|
||||
std::string id() {
|
||||
std::random_device random;
|
||||
std::ostringstream stream;
|
||||
stream << std::hex << std::setfill('0');
|
||||
for (int i = 0; i < 4; ++i)
|
||||
stream << std::setw(8) << random();
|
||||
return stream.str();
|
||||
}
|
||||
int create(void* user, const char* input, FasetWrite write, void* receiver) {
|
||||
try {
|
||||
auto& state = *static_cast<State*>(user);
|
||||
const auto arguments = Json::parse(input);
|
||||
const auto document =
|
||||
invoke(state, "faset_document_query", {{"document", arguments.at("document")}});
|
||||
const auto schema = invoke(state, "faset_schema", Json::object());
|
||||
bool known = false;
|
||||
for (const auto& type : schema.at("types"))
|
||||
if (type.at("id") == "example.beacon")
|
||||
known = true;
|
||||
if (!known)
|
||||
throw std::runtime_error("Include Beacon.hpp in gameplay, register its schema and "
|
||||
"behavior, then Build before adding a Beacon");
|
||||
const auto entity = id();
|
||||
Json record = {
|
||||
{"id", entity},
|
||||
{"name", "Beacon"},
|
||||
{"parent", nullptr},
|
||||
{"components",
|
||||
Json::array(
|
||||
{{{"id", id()},
|
||||
{"type", "faset.transform"},
|
||||
{"version", 1},
|
||||
{"fields",
|
||||
{{"position", {0, 1, 0}}, {"rotation", {0, 0, 0}}, {"scale", {1, 1, 1}}}}},
|
||||
{{"id", id()},
|
||||
{"type", "faset.mesh"},
|
||||
{"version", 1},
|
||||
{"fields",
|
||||
{{"asset", ""}, {"primitive", "cube"}, {"color", {0.68, 0.55, 0.95, 1}}}}},
|
||||
{{"id", id()},
|
||||
{"type", "example.beacon"},
|
||||
{"version", 1},
|
||||
{"fields", {{"speed", 1.0}}}}})}};
|
||||
const auto result =
|
||||
invoke(state, "faset_scene_edit",
|
||||
{{"document", document.at("id")},
|
||||
{"revision", document.at("revision")},
|
||||
{"operations", Json::array({{{"op", "entity.create"}, {"entity", record}}})}})
|
||||
.dump();
|
||||
write(receiver, result.data(), result.size());
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
const auto output = Json{{"code", "beacon.create"}, {"message", error.what()}}.dump();
|
||||
write(receiver, output.data(), output.size());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
void shutdown(void* user) {
|
||||
delete static_cast<State*>(user);
|
||||
}
|
||||
} // namespace
|
||||
extern "C" FASET_PLUGIN_EXPORT int faset_editor_plugin(const FasetEditorHost* host,
|
||||
FasetEditorPlugin* plugin) {
|
||||
if (!host || !plugin || host->api_version != FASET_EDITOR_API_VERSION ||
|
||||
host->struct_size != sizeof(FasetEditorHost) ||
|
||||
std::string(host->build_fingerprint) != FASET_EDITOR_SDK_FINGERPRINT)
|
||||
return 1;
|
||||
try {
|
||||
auto* state = new State{host};
|
||||
*plugin = {FASET_EDITOR_API_VERSION, sizeof(FasetEditorPlugin),
|
||||
FASET_EDITOR_SDK_FINGERPRINT, state, shutdown};
|
||||
const auto descriptor = Json{
|
||||
{"name", "plugin_example_beacon_create"},
|
||||
{"description", "Create a rotating Beacon using one authoring transaction."},
|
||||
{"inputSchema",
|
||||
{{"type", "object"},
|
||||
{"properties", {{"document", {{"type", "string"}}}}},
|
||||
{"required", {"document"}},
|
||||
{"additionalProperties",
|
||||
false}}}}.dump();
|
||||
if (host->register_command(host->context, descriptor.c_str(), create, state) != 0)
|
||||
return 1;
|
||||
const auto panel = Json{{"id", "example.beacon.tools"},
|
||||
{"title", "Beacon tools"},
|
||||
{"action", "Add Beacon"},
|
||||
{"command", "plugin_example_beacon_create"},
|
||||
{"arguments", {{"document", "$document"}}}}
|
||||
.dump();
|
||||
return host->register_panel(host->context, panel.c_str());
|
||||
} catch (...) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -6,46 +6,65 @@
|
||||
namespace faset::gameplay {
|
||||
void registerGameplay(runtime::Runtime& engine) {
|
||||
runtime::Behavior character;
|
||||
character.fixedUpdate=[](runtime::Runtime& world,runtime::EntityHandle self,double) {
|
||||
const auto fields=world.fields(self,"gameplay.character");
|
||||
auto velocity=world.velocity(self);const auto input=world.input();
|
||||
velocity[0]=input.horizontal*fields.value("speed",4.0f);
|
||||
// A minimal demo controller: jump only near zero vertical velocity.
|
||||
// A production grounded controller needs contact normals / a ground query.
|
||||
if(input.jumpPressed&&std::abs(velocity[1])<0.1f)velocity[1]=fields.value("jump_speed",5.0f);
|
||||
world.setVelocity(self,velocity);
|
||||
character.fixedUpdate = [](runtime::Runtime& world, runtime::EntityHandle self, double) {
|
||||
const auto fields = world.fields(self, "gameplay.character");
|
||||
auto velocity = world.velocity(self);
|
||||
const auto input = world.input();
|
||||
velocity[0] = input.horizontal * fields.value("speed", 4.0f);
|
||||
if (input.jumpPressed && world.grounded(self))
|
||||
velocity[1] = fields.value("jump_speed", 5.0f);
|
||||
world.setVelocity(self, velocity);
|
||||
};
|
||||
engine.registerBehavior("gameplay.character",std::move(character));
|
||||
engine.registerBehavior("gameplay.character", std::move(character));
|
||||
|
||||
runtime::Behavior door;
|
||||
auto open=std::make_shared<std::map<std::pair<std::uint64_t,std::uint32_t>,bool>>();
|
||||
door.onStart=[open](runtime::Runtime&,runtime::EntityHandle self,double){(*open)[{self.session,self.slot}]=false;};
|
||||
door.onDestroy=[open](runtime::Runtime&,runtime::EntityHandle self,double){open->erase({self.session,self.slot});};
|
||||
door.fixedUpdate=[open](runtime::Runtime& world,runtime::EntityHandle self,double dt) {
|
||||
const auto fields=world.fields(self,"gameplay.door");
|
||||
auto pose=world.transform(self);
|
||||
auto& opened=(*open)[{self.session,self.slot}];
|
||||
if(world.input().interactPressed)opened=!opened;
|
||||
const float target=opened?fields.value("open_angle",1.5707963f):fields.value("closed_angle",0.0f);
|
||||
const float distance=target-pose.rotation[1];
|
||||
const float amount=std::max(0.0f,fields.value("speed",1.5f))*static_cast<float>(dt);
|
||||
pose.rotation[1]+=std::clamp(distance,-amount,amount);
|
||||
world.setTransform(self,pose);
|
||||
auto open = std::make_shared<std::map<std::pair<std::uint64_t, std::uint32_t>, bool>>();
|
||||
door.onStart = [open](runtime::Runtime&, runtime::EntityHandle self, double) {
|
||||
(*open)[{self.session, self.slot}] = false;
|
||||
};
|
||||
engine.registerBehavior("gameplay.door",std::move(door));
|
||||
door.onDestroy = [open](runtime::Runtime&, runtime::EntityHandle self, double) {
|
||||
open->erase({self.session, self.slot});
|
||||
};
|
||||
door.fixedUpdate = [open](runtime::Runtime& world, runtime::EntityHandle self, double dt) {
|
||||
const auto fields = world.fields(self, "gameplay.door");
|
||||
auto pose = world.transform(self);
|
||||
auto& opened = (*open)[{self.session, self.slot}];
|
||||
if (world.input().interactPressed)
|
||||
opened = !opened;
|
||||
const float target =
|
||||
opened ? fields.value("open_angle", 1.5707963f) : fields.value("closed_angle", 0.0f);
|
||||
const float distance = target - pose.rotation[1];
|
||||
const float amount = std::max(0.0f, fields.value("speed", 1.5f)) * static_cast<float>(dt);
|
||||
pose.rotation[1] += std::clamp(distance, -amount, amount);
|
||||
world.setTransform(self, pose);
|
||||
};
|
||||
engine.registerBehavior("gameplay.door", std::move(door));
|
||||
}
|
||||
|
||||
nlohmann::json schema() {
|
||||
// Explicit declarations shared by Player and SchemaExporter. This function
|
||||
// constructs descriptions only: no Runtime, physics world or lifecycle.
|
||||
return nlohmann::json::array({
|
||||
{{"id","gameplay.character"},{"version",1},{"name","Character"},{"fields",{
|
||||
{"speed",{{"id","speed"},{"type","number"},{"default",4.0},{"min",0.0}}},
|
||||
{"jump_speed",{{"id","jump_speed"},{"type","number"},{"default",5.0},{"min",0.0}}}}}},
|
||||
{{"id","gameplay.door"},{"version",1},{"name","Door"},{"fields",{
|
||||
{"open_angle",{{"id","open_angle"},{"type","number"},{"default",1.5707963},{"units","rad"}}},
|
||||
{"closed_angle",{{"id","closed_angle"},{"type","number"},{"default",0.0},{"units","rad"}}},
|
||||
{"speed",{{"id","speed"},{"type","number"},{"default",1.5},{"min",0.0},{"units","rad/s"}}}}}}
|
||||
});
|
||||
}
|
||||
return nlohmann::json::array(
|
||||
{{{"id", "gameplay.character"},
|
||||
{"version", 1},
|
||||
{"name", "Character"},
|
||||
{"fields",
|
||||
{{"speed", {{"id", "speed"}, {"type", "number"}, {"default", 4.0}, {"min", 0.0}}},
|
||||
{"jump_speed",
|
||||
{{"id", "jump_speed"}, {"type", "number"}, {"default", 5.0}, {"min", 0.0}}}}}},
|
||||
{{"id", "gameplay.door"},
|
||||
{"version", 1},
|
||||
{"name", "Door"},
|
||||
{"fields",
|
||||
{{"open_angle",
|
||||
{{"id", "open_angle"}, {"type", "number"}, {"default", 1.5707963}, {"units", "rad"}}},
|
||||
{"closed_angle",
|
||||
{{"id", "closed_angle"}, {"type", "number"}, {"default", 0.0}, {"units", "rad"}}},
|
||||
{"speed",
|
||||
{{"id", "speed"},
|
||||
{"type", "number"},
|
||||
{"default", 1.5},
|
||||
{"min", 0.0},
|
||||
{"units", "rad/s"}}}}}}});
|
||||
}
|
||||
} // namespace faset::gameplay
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
namespace faset::gameplay {
|
||||
void registerGameplay(runtime::Runtime& runtime);
|
||||
nlohmann::json schema();
|
||||
}
|
||||
} // namespace faset::gameplay
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "Gameplay.hpp"
|
||||
|
||||
namespace faset::gameplay {
|
||||
void registerGameplay(runtime::Runtime& world) {
|
||||
runtime::Behavior motion;
|
||||
motion.fixedUpdate = [](runtime::Runtime& game, runtime::EntityHandle self, double delta) {
|
||||
auto pose = game.transform(self);
|
||||
pose.position[0] += game.fields(self, "tutorial.fixed_move").value("speed", 2.0f) *
|
||||
static_cast<float>(delta);
|
||||
game.setTransform(self, pose);
|
||||
};
|
||||
world.registerBehavior("tutorial.fixed_move", std::move(motion));
|
||||
|
||||
runtime::Behavior follow;
|
||||
follow.lateUpdate = [](runtime::Runtime& game, runtime::EntityHandle self, double) {
|
||||
const auto settings = game.fields(self, "tutorial.follow");
|
||||
const auto target = game.find(settings.value("target", std::string{}));
|
||||
if (!game.valid(target))
|
||||
return; // Target may be absent or destroyed.
|
||||
const auto targetPose = game.presentation(target); // Already interpolated.
|
||||
const auto offset = settings.at("offset").get<runtime::Vec3>();
|
||||
auto cameraPose = game.presentation(self);
|
||||
for (int axis = 0; axis < 3; ++axis)
|
||||
cameraPose.position[axis] = targetPose.position[axis] + offset[axis];
|
||||
game.setPresentation(self, cameraPose); // Does not write the simulation pose.
|
||||
};
|
||||
world.registerBehavior("tutorial.follow", std::move(follow));
|
||||
}
|
||||
|
||||
nlohmann::json schema() {
|
||||
return nlohmann::json::array(
|
||||
{{{"id", "tutorial.fixed_move"},
|
||||
{"version", 1},
|
||||
{"name", "Fixed movement"},
|
||||
{"fields", {{"speed", {{"id", "speed"}, {"type", "number"}, {"default", 2.0}}}}}},
|
||||
{{"id", "tutorial.follow"},
|
||||
{"version", 1},
|
||||
{"name", "Follow presentation"},
|
||||
{"fields",
|
||||
{{"target", {{"id", "target"}, {"type", "entity_ref"}, {"default", "actor"}}},
|
||||
{"offset", {{"id", "offset"}, {"type", "vec3"}, {"default", {0, 0, 10}}}}}}}});
|
||||
}
|
||||
} // namespace faset::gameplay
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <faset/runtime/Runtime.hpp>
|
||||
|
||||
namespace faset::gameplay {
|
||||
void registerGameplay(runtime::Runtime& world);
|
||||
nlohmann::json schema();
|
||||
} // namespace faset::gameplay
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"format": "faset.scene",
|
||||
"version": 1,
|
||||
"id": "tutorial-following",
|
||||
"name": "Following tutorial",
|
||||
"dimension": 2,
|
||||
"simulation": {
|
||||
"fixed_delta": 0.016666666666666666,
|
||||
"max_catch_up_ticks": 4,
|
||||
"physics_substeps": 4,
|
||||
"gravity": [
|
||||
0,
|
||||
-9.81,
|
||||
0
|
||||
]
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"id": "actor",
|
||||
"name": "actor",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "actor-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "actor-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
0.2,
|
||||
0.7,
|
||||
0.9,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "actor-tutorial.fixed_move",
|
||||
"type": "tutorial.fixed_move",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"speed": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "camera",
|
||||
"name": "camera",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "camera-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
10
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "camera-faset.camera",
|
||||
"type": "faset.camera",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"fov": 60,
|
||||
"near": 0.1,
|
||||
"far": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "camera-tutorial.follow",
|
||||
"type": "tutorial.follow",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"target": "actor",
|
||||
"offset": [
|
||||
0,
|
||||
0,
|
||||
10
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"instances": []
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "Gameplay.hpp"
|
||||
|
||||
namespace faset::gameplay {
|
||||
void registerGameplay(runtime::Runtime& world) {
|
||||
runtime::Behavior mover;
|
||||
mover.update = [](runtime::Runtime& game, runtime::EntityHandle self, double delta) {
|
||||
// fields() returns a configuration copy. transform() returns a live pose copy.
|
||||
const auto settings = game.fields(self, "tutorial.move_x");
|
||||
const float speed = settings.value("speed", 2.0f); // metres per second
|
||||
auto pose = game.transform(self);
|
||||
pose.position[0] += speed * static_cast<float>(delta);
|
||||
game.setTransform(self, pose); // This object has no rigid body.
|
||||
};
|
||||
world.registerBehavior("tutorial.move_x", std::move(mover));
|
||||
}
|
||||
|
||||
nlohmann::json schema() {
|
||||
// The FieldId is the map key. Keep it stable if you change a display name.
|
||||
return nlohmann::json::array({{{"id", "tutorial.move_x"},
|
||||
{"version", 1},
|
||||
{"name", "Move along X"},
|
||||
{"fields",
|
||||
{{"speed",
|
||||
{{"id", "speed"},
|
||||
{"type", "number"},
|
||||
{"default", 2.0},
|
||||
{"min", -20.0},
|
||||
{"max", 20.0},
|
||||
{"units", "m/s"}}}}}}});
|
||||
}
|
||||
} // namespace faset::gameplay
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <faset/runtime/Runtime.hpp>
|
||||
|
||||
namespace faset::gameplay {
|
||||
void registerGameplay(runtime::Runtime& world);
|
||||
nlohmann::json schema();
|
||||
} // namespace faset::gameplay
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"format": "faset.scene",
|
||||
"version": 1,
|
||||
"id": "tutorial-moving",
|
||||
"name": "Moving tutorial",
|
||||
"dimension": 2,
|
||||
"simulation": {
|
||||
"fixed_delta": 0.016666666666666666,
|
||||
"max_catch_up_ticks": 4,
|
||||
"physics_substeps": 4,
|
||||
"gravity": [
|
||||
0,
|
||||
-9.81,
|
||||
0
|
||||
]
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"id": "actor",
|
||||
"name": "actor",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "actor-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "actor-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
0.2,
|
||||
0.7,
|
||||
0.9,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "actor-tutorial.move_x",
|
||||
"type": "tutorial.move_x",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"speed": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"instances": []
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "Gameplay.hpp"
|
||||
#include <iostream>
|
||||
|
||||
namespace faset::gameplay {
|
||||
void registerGameplay(runtime::Runtime& world) {
|
||||
runtime::Behavior character;
|
||||
character.fixedUpdate = [](runtime::Runtime& game, runtime::EntityHandle self, double) {
|
||||
const auto settings = game.fields(self, "tutorial.character");
|
||||
const auto input = game.input();
|
||||
auto velocity = game.velocity(self); // Metres per second; preserve the Y component.
|
||||
velocity[0] = input.horizontal * settings.value("speed", 4.0f);
|
||||
if (input.jumpPressed && game.grounded(self))
|
||||
velocity[1] = settings.value("jump_speed", 5.0f);
|
||||
// Do not multiply velocity by delta. The physics solver integrates it.
|
||||
game.setVelocity(self, velocity);
|
||||
};
|
||||
character.onCollision = [](runtime::Runtime&, runtime::EntityHandle,
|
||||
const runtime::CollisionEvent& event) {
|
||||
if (event.began)
|
||||
std::cout << "Character contact began\n";
|
||||
};
|
||||
world.registerBehavior("tutorial.character", std::move(character));
|
||||
}
|
||||
|
||||
nlohmann::json schema() {
|
||||
return nlohmann::json::array({{{"id", "tutorial.character"},
|
||||
{"version", 1},
|
||||
{"name", "Physics character"},
|
||||
{"fields",
|
||||
{{"speed",
|
||||
{{"id", "speed"},
|
||||
{"type", "number"},
|
||||
{"default", 4.0},
|
||||
{"min", 0.0},
|
||||
{"units", "m/s"}}},
|
||||
{"jump_speed",
|
||||
{{"id", "jump_speed"},
|
||||
{"type", "number"},
|
||||
{"default", 5.0},
|
||||
{"min", 0.0},
|
||||
{"units", "m/s"}}}}}}});
|
||||
}
|
||||
} // namespace faset::gameplay
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <faset/runtime/Runtime.hpp>
|
||||
|
||||
namespace faset::gameplay {
|
||||
void registerGameplay(runtime::Runtime& world);
|
||||
nlohmann::json schema();
|
||||
} // namespace faset::gameplay
|
||||
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"format": "faset.scene",
|
||||
"version": 1,
|
||||
"id": "tutorial-physics",
|
||||
"name": "Physics tutorial",
|
||||
"dimension": 2,
|
||||
"simulation": {
|
||||
"fixed_delta": 0.016666666666666666,
|
||||
"max_catch_up_ticks": 4,
|
||||
"physics_substeps": 4,
|
||||
"gravity": [
|
||||
0,
|
||||
-9.81,
|
||||
0
|
||||
]
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"id": "floor",
|
||||
"name": "floor",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "floor-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
-0.5,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "floor-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
0.4,
|
||||
0.5,
|
||||
0.6,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
12,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "floor-faset.rigid_body_2d",
|
||||
"type": "faset.rigid_body_2d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
6,
|
||||
0.5
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "actor",
|
||||
"name": "actor",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "actor-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
0.5,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "actor-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
0.2,
|
||||
0.7,
|
||||
0.9,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "actor-faset.rigid_body_2d",
|
||||
"type": "faset.rigid_body_2d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "dynamic",
|
||||
"half_extents": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.3,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "actor-tutorial.character",
|
||||
"type": "tutorial.character",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"speed": 4,
|
||||
"jump_speed": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"instances": []
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "Gameplay.hpp"
|
||||
#include <map>
|
||||
#include <tuple>
|
||||
|
||||
namespace faset::gameplay {
|
||||
void registerGameplay(runtime::Runtime& world) {
|
||||
runtime::Behavior spawner;
|
||||
spawner.onStart = [](runtime::Runtime& game, runtime::EntityHandle self, double) {
|
||||
const auto settings = game.fields(self, "tutorial.spawn_once");
|
||||
const auto id = settings.at("spawned_id").get<std::string>();
|
||||
// This queues a runtime object; it does not change the saved scene.
|
||||
game.spawn(
|
||||
{{"id", id},
|
||||
{"name", "Temporary box"},
|
||||
{"parent", nullptr},
|
||||
{"components", nlohmann::json::array(
|
||||
{{{"id", id + "/transform"},
|
||||
{"type", "faset.transform"},
|
||||
{"version", 1},
|
||||
{"fields", {{"position", {0, 0, 0}}}}},
|
||||
{{"id", id + "/sprite"},
|
||||
{"type", "faset.sprite"},
|
||||
{"version", 1},
|
||||
{"fields", {{"color", {0.9, 0.6, 0.2, 1}}, {"size", {1, 1}}}}},
|
||||
{{"id", id + "/lifetime"},
|
||||
{"type", "tutorial.timed_despawn"},
|
||||
{"version", 1},
|
||||
{"fields", {{"seconds", settings.value("lifetime", 1.0)}}}}})}});
|
||||
};
|
||||
world.registerBehavior("tutorial.spawn_once", std::move(spawner));
|
||||
|
||||
// Ordinary C++ state belongs to the gameplay module. Every handle part matters.
|
||||
using Key = std::tuple<std::uint64_t, std::uint32_t, std::uint64_t>;
|
||||
auto ages = std::make_shared<std::map<Key, double>>();
|
||||
auto key = [](runtime::EntityHandle h) { return Key{h.session, h.slot, h.generation}; };
|
||||
runtime::Behavior lifetime;
|
||||
lifetime.onStart = [ages, key](runtime::Runtime&, runtime::EntityHandle self, double) {
|
||||
(*ages)[key(self)] = 0.0;
|
||||
};
|
||||
lifetime.fixedUpdate = [ages, key](runtime::Runtime& game, runtime::EntityHandle self,
|
||||
double delta) {
|
||||
auto& age = ages->at(key(self));
|
||||
age += delta;
|
||||
if (age >= game.fields(self, "tutorial.timed_despawn").value("seconds", 1.0))
|
||||
game.destroy(self); // Still valid until the next fixed-tick barrier.
|
||||
};
|
||||
lifetime.onDestroy = [ages, key](runtime::Runtime&, runtime::EntityHandle self, double) {
|
||||
ages->erase(key(self));
|
||||
};
|
||||
world.registerBehavior("tutorial.timed_despawn", std::move(lifetime));
|
||||
}
|
||||
|
||||
nlohmann::json schema() {
|
||||
return nlohmann::json::array(
|
||||
{{{"id", "tutorial.spawn_once"},
|
||||
{"version", 1},
|
||||
{"name", "Spawn once"},
|
||||
{"fields",
|
||||
{{"spawned_id",
|
||||
{{"id", "spawned_id"}, {"type", "string"}, {"default", "temporary-box"}}},
|
||||
{"lifetime",
|
||||
{{"id", "lifetime"}, {"type", "number"}, {"default", 1.0}, {"min", 0.0}}}}}},
|
||||
{{"id", "tutorial.timed_despawn"},
|
||||
{"version", 1},
|
||||
{"name", "Timed despawn"},
|
||||
{"fields",
|
||||
{{"seconds",
|
||||
{{"id", "seconds"}, {"type", "number"}, {"default", 1.0}, {"min", 0.0}}}}}}});
|
||||
}
|
||||
} // namespace faset::gameplay
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <faset/runtime/Runtime.hpp>
|
||||
|
||||
namespace faset::gameplay {
|
||||
void registerGameplay(runtime::Runtime& world);
|
||||
nlohmann::json schema();
|
||||
} // namespace faset::gameplay
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"format": "faset.scene",
|
||||
"version": 1,
|
||||
"id": "tutorial-spawning",
|
||||
"name": "Spawning tutorial",
|
||||
"dimension": 2,
|
||||
"simulation": {
|
||||
"fixed_delta": 0.016666666666666666,
|
||||
"max_catch_up_ticks": 4,
|
||||
"physics_substeps": 4,
|
||||
"gravity": [
|
||||
0,
|
||||
-9.81,
|
||||
0
|
||||
]
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"id": "spawner",
|
||||
"name": "spawner",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "spawner-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "spawner-tutorial.spawn_once",
|
||||
"type": "tutorial.spawn_once",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"spawned_id": "temporary-box",
|
||||
"lifetime": 1.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"instances": []
|
||||
}
|
||||
Reference in New Issue
Block a user