Checkpoint 3: deliver playable samples and complete native authoring workflows
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.faset/
|
||||
Exports/
|
||||
@@ -0,0 +1,55 @@
|
||||
# Collect & Escape 2D
|
||||
|
||||
A small playable project using the real Faset C++ gameplay module and Box2D adapter. All geometry is built in, so this project does not need an asset import or Blender installation.
|
||||
|
||||
## Play
|
||||
|
||||
Open it from the repository root with a built Editor:
|
||||
|
||||
```bash
|
||||
build/linux-debug/faset_editor --project examples/projects/collect-2d
|
||||
```
|
||||
|
||||
Use **Play** to build this project's `Scripts/Gameplay.cpp` and start a separate Player. On Windows use your Windows build directory and `faset_editor.exe`. First complete the engine build setup in the User Manual.
|
||||
|
||||
For a direct Player build on Linux, from the repository root:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build/collect-2d -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DFASET_GAMEPLAY_SOURCE_DIR="$PWD/examples/projects/collect-2d/Scripts"
|
||||
cmake --build build/collect-2d --target faset_player faset_schema_exporter
|
||||
build/collect-2d/faset_player --scene examples/projects/collect-2d/Scenes/main.scene.json
|
||||
```
|
||||
|
||||
On Windows use a Developer shell with `clang-cl` and the Windows SDK, select `clang-cl` for both compiler options, and give `FASET_GAMEPLAY_SOURCE_DIR` an absolute path.
|
||||
|
||||
## Objective and controls
|
||||
|
||||
- A/D or left/right arrows move on X.
|
||||
- **Space** jumps while a native contact supports the player block.
|
||||
- Collect all three **gold cubes**. Jump onto the raised platform for the middle pickup.
|
||||
- Each pickup moves to the progress display. After all three, the **red physical gate** moves out of the level.
|
||||
- Reach the **green exit** to reveal the large gold victory marker. Movement stops after completion.
|
||||
- **E** restores the player, pickups, gate, and victory state. Falling below the level also resets the round.
|
||||
- **P** pauses, **N** advances one fixed tick, and **Escape** closes the Player.
|
||||
|
||||
The Player window title and console identify the controls and objective; the game uses geometric progress markers rather than a text HUD. The player is a dynamic box with zero friction, not an articulated character controller. It can rotate after impacts.
|
||||
|
||||
## Inspect and change the game
|
||||
|
||||
`project.faset.json` chooses `Scenes/main.scene.json`. The scene stores object/component IDs, transforms, physics settings, and entity references in the collector component. `Scripts/Gameplay.cpp` owns the round's transient C++ state and registers its editable schema. `speed` and `jump_speed` use metres per second.
|
||||
|
||||
Change JSON and restart Play to see new level data. Change C++ or its schema, then stop, build, and restart; C++ hot reload is not implemented. Runtime pickup progress does not rewrite the authoring scene or create Undo entries.
|
||||
|
||||
## Verify
|
||||
|
||||
The normal test build creates `faset_example_2d_tests`. It compiles this exact module, follows a route using movement/jump input, collects the elevated pickup, opens the gate, reaches the exit, resets the round, and restarts the scene. It does not teleport to pass the objective.
|
||||
|
||||
```bash
|
||||
cmake --build build/linux-debug --target faset_example_2d_tests
|
||||
ctest --test-dir build/linux-debug -R '^playable_2d$' --output-on-failure
|
||||
```
|
||||
|
||||
The runtime test is independent of graphics. A Player run additionally requires the supported Vulkan platform setup. The Editor's Export action creates a separate Release build and a cooked standalone generation; run that generation's Player from its export directory.
|
||||
@@ -0,0 +1,659 @@
|
||||
{
|
||||
"format": "faset.scene",
|
||||
"version": 1,
|
||||
"id": "example-collect-2d-scene",
|
||||
"name": "Collect & Escape 2D \u2014 WASD / Space / E reset",
|
||||
"dimension": 2,
|
||||
"simulation": {
|
||||
"fixed_delta": 0.016666666666666666,
|
||||
"max_catch_up_ticks": 4,
|
||||
"physics_substeps": 4,
|
||||
"gravity": [
|
||||
0,
|
||||
-9.81,
|
||||
0
|
||||
]
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"id": "floor",
|
||||
"name": "Ground",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "floor-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
-0.75,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "floor-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
0.2,
|
||||
0.3,
|
||||
0.39,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
21,
|
||||
0.5
|
||||
],
|
||||
"texture": "",
|
||||
"layer": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "floor-faset.rigid_body_2d",
|
||||
"type": "faset.rigid_body_2d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
10.5,
|
||||
0.25
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "platform",
|
||||
"name": "Jump platform",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "platform-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "platform-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
0.35,
|
||||
0.44,
|
||||
0.55,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
2,
|
||||
1
|
||||
],
|
||||
"texture": "",
|
||||
"layer": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "platform-faset.rigid_body_2d",
|
||||
"type": "faset.rigid_body_2d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
1.0,
|
||||
0.5
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "left-wall",
|
||||
"name": "Left boundary",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "left-wall-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
-10.25,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "left-wall-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
0.2,
|
||||
0.3,
|
||||
0.39,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
0.5,
|
||||
3.5
|
||||
],
|
||||
"texture": "",
|
||||
"layer": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "left-wall-faset.rigid_body_2d",
|
||||
"type": "faset.rigid_body_2d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
0.25,
|
||||
1.75
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "right-wall",
|
||||
"name": "Right boundary",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "right-wall-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
10.25,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "right-wall-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
0.2,
|
||||
0.3,
|
||||
0.39,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
0.5,
|
||||
3.5
|
||||
],
|
||||
"texture": "",
|
||||
"layer": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "right-wall-faset.rigid_body_2d",
|
||||
"type": "faset.rigid_body_2d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
0.25,
|
||||
1.75
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "token_1",
|
||||
"name": "Gold pickup 1",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "token_1-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
-5,
|
||||
0.35,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "token_1-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
1,
|
||||
0.68,
|
||||
0.14,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"texture": "",
|
||||
"layer": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "token_2",
|
||||
"name": "Gold pickup 2",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "token_2-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
1.35,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "token_2-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
1,
|
||||
0.68,
|
||||
0.14,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"texture": "",
|
||||
"layer": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "token_3",
|
||||
"name": "Gold pickup 3",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "token_3-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
5,
|
||||
0.35,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "token_3-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
1,
|
||||
0.68,
|
||||
0.14,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"texture": "",
|
||||
"layer": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "gate",
|
||||
"name": "Collect all three to open",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "gate-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
7,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gate-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
0.9,
|
||||
0.23,
|
||||
0.16,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
0.45,
|
||||
3
|
||||
],
|
||||
"texture": "",
|
||||
"layer": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gate-faset.rigid_body_2d",
|
||||
"type": "faset.rigid_body_2d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
0.225,
|
||||
1.5
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "goal",
|
||||
"name": "Green exit",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "goal-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
8.5,
|
||||
0.15,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "goal-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
0.15,
|
||||
0.9,
|
||||
0.47,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
0.85,
|
||||
0.3
|
||||
],
|
||||
"texture": "",
|
||||
"layer": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "win_marker",
|
||||
"name": "Victory marker",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "win_marker-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
8.5,
|
||||
3.0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "win_marker-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
1,
|
||||
0.68,
|
||||
0.14,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"texture": "",
|
||||
"layer": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "player",
|
||||
"name": "Blue player block",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "player-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
-8,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "player-faset.sprite",
|
||||
"type": "faset.sprite",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"color": [
|
||||
0.12,
|
||||
0.65,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"texture": "",
|
||||
"layer": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "player-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,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "player-example.collector_2d",
|
||||
"type": "example.collector_2d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"speed": 4,
|
||||
"jump_speed": 6,
|
||||
"gate": "gate",
|
||||
"goal": "goal",
|
||||
"win_marker": "win_marker",
|
||||
"token_1": "token_1",
|
||||
"token_2": "token_2",
|
||||
"token_3": "token_3"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"instances": []
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
#include "Gameplay.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
|
||||
namespace faset::gameplay {
|
||||
namespace {
|
||||
constexpr bool is3D = false;
|
||||
constexpr const char* collectorType = "example.collector_2d";
|
||||
using Key = std::tuple<std::uint64_t, std::uint32_t, std::uint64_t>;
|
||||
Key key(runtime::EntityHandle handle) {
|
||||
return {handle.session, handle.slot, handle.generation};
|
||||
}
|
||||
struct Round {
|
||||
runtime::Transform spawn, gatePose, victoryPose;
|
||||
runtime::EntityHandle gate, goal, victory;
|
||||
std::array<runtime::EntityHandle, 3> tokens;
|
||||
std::array<runtime::Transform, 3> tokenPoses;
|
||||
std::array<bool, 3> collected{};
|
||||
bool won{};
|
||||
bool gateOpened{};
|
||||
};
|
||||
float distance(runtime::Vec3 a, runtime::Vec3 b) {
|
||||
const float x = a[0] - b[0], y = a[1] - b[1], z = is3D ? a[2] - b[2] : 0;
|
||||
return std::sqrt(x * x + y * y + z * z);
|
||||
}
|
||||
void reset(runtime::Runtime& game, runtime::EntityHandle self, Round& round) {
|
||||
game.teleport(self, round.spawn);
|
||||
game.setVelocity(self, {0, 0, 0});
|
||||
game.teleport(round.gate, round.gatePose);
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
game.setTransform(round.tokens[i], round.tokenPoses[i]);
|
||||
auto hidden = round.victoryPose;
|
||||
hidden.position[1] = -50;
|
||||
game.setTransform(round.victory, hidden);
|
||||
round.collected = {};
|
||||
round.won = false;
|
||||
round.gateOpened = false;
|
||||
std::cout << "New round: collect the three gold cubes, then reach the green exit. E resets.\n";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void registerGameplay(runtime::Runtime& world) {
|
||||
|
||||
auto rounds = std::make_shared<std::map<Key, Round>>();
|
||||
runtime::Behavior collector;
|
||||
collector.onStart = [rounds](runtime::Runtime& game, runtime::EntityHandle self, double) {
|
||||
const auto fields = game.fields(self, collectorType);
|
||||
Round round;
|
||||
round.spawn = game.transform(self);
|
||||
auto resolve = [&](const char* field) {
|
||||
const auto handle = game.find(fields.at(field).get<std::string>());
|
||||
if (!game.valid(handle))
|
||||
throw std::runtime_error(std::string("Missing level reference: ") + field);
|
||||
return handle;
|
||||
};
|
||||
round.gate = resolve("gate");
|
||||
round.goal = resolve("goal");
|
||||
round.victory = resolve("win_marker");
|
||||
round.gatePose = game.transform(round.gate);
|
||||
round.victoryPose = game.transform(round.victory);
|
||||
for (std::size_t i = 0; i < 3; ++i) {
|
||||
const auto field = "token_" + std::to_string(i + 1);
|
||||
round.tokens[i] = resolve(field.c_str());
|
||||
round.tokenPoses[i] = game.transform(round.tokens[i]);
|
||||
}
|
||||
auto& stored = rounds->insert_or_assign(key(self), std::move(round)).first->second;
|
||||
reset(game, self, stored);
|
||||
};
|
||||
collector.onDestroy = [rounds](runtime::Runtime&, runtime::EntityHandle self, double) {
|
||||
rounds->erase(key(self));
|
||||
};
|
||||
collector.fixedUpdate = [rounds](runtime::Runtime& game, runtime::EntityHandle self, double) {
|
||||
auto& round = rounds->at(key(self));
|
||||
const auto input = game.input();
|
||||
const auto position = game.transform(self).position;
|
||||
if (input.interactPressed || position[1] < -8) {
|
||||
reset(game, self, round);
|
||||
return;
|
||||
}
|
||||
const auto fields = game.fields(self, collectorType);
|
||||
auto velocity = game.velocity(self);
|
||||
float x = input.horizontal, z = is3D ? -input.vertical : 0;
|
||||
const float length = std::sqrt(x * x + z * z);
|
||||
if (length > 1) {
|
||||
x /= length;
|
||||
z /= length;
|
||||
}
|
||||
const auto speed = fields.value("speed", 4.0f);
|
||||
velocity[0] = round.won ? 0 : x * speed;
|
||||
if (is3D)
|
||||
velocity[2] = round.won ? 0 : z * speed;
|
||||
if (!round.won && input.jumpPressed && game.grounded(self))
|
||||
velocity[1] = fields.value("jump_speed", 6.0f);
|
||||
game.setVelocity(self, velocity);
|
||||
if (round.won)
|
||||
return;
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
if (!round.collected[i] && distance(position, round.tokenPoses[i].position) < 0.95f) {
|
||||
round.collected[i] = true;
|
||||
auto display = round.tokenPoses[i];
|
||||
display.position = is3D ? runtime::Vec3{6, 2 + float(i) * 0.8f, -4}
|
||||
: runtime::Vec3{-1.2f + float(i) * 1.2f, 4.5f, 0};
|
||||
game.setTransform(round.tokens[i], display);
|
||||
std::cout << "Collected "
|
||||
<< std::count(round.collected.begin(), round.collected.end(), true)
|
||||
<< "/3\n";
|
||||
}
|
||||
if (std::all_of(round.collected.begin(), round.collected.end(),
|
||||
[](bool value) { return value; })) {
|
||||
if (!round.gateOpened) {
|
||||
auto open = round.gatePose;
|
||||
open.position[1] = -30;
|
||||
game.teleport(round.gate, open);
|
||||
round.gateOpened = true;
|
||||
}
|
||||
if (distance(position, game.transform(round.goal).position) < 1.0f) {
|
||||
round.won = true;
|
||||
game.setTransform(round.victory, round.victoryPose);
|
||||
std::cout << "Level complete! The gold victory marker is visible. Press E to play "
|
||||
"again.\n";
|
||||
}
|
||||
}
|
||||
};
|
||||
world.registerBehavior(collectorType, std::move(collector));
|
||||
}
|
||||
|
||||
nlohmann::json schema() {
|
||||
nlohmann::json fields = {{"speed",
|
||||
{{"id", "speed"},
|
||||
{"name", "Move speed"},
|
||||
{"type", "number"},
|
||||
{"default", 4.0},
|
||||
{"min", 0.0},
|
||||
{"units", "m/s"}}},
|
||||
{"jump_speed",
|
||||
{{"id", "jump_speed"},
|
||||
{"name", "Jump speed"},
|
||||
{"type", "number"},
|
||||
{"default", 6.0},
|
||||
{"min", 0.0},
|
||||
{"units", "m/s"}}}};
|
||||
for (const auto* id : {"gate", "goal", "win_marker", "token_1", "token_2", "token_3"})
|
||||
fields[id] = {{"id", id}, {"name", id}, {"type", "entity_ref"}, {"default", id}};
|
||||
auto types = nlohmann::json::array({{{"id", collectorType},
|
||||
{"name", "Collect three and escape"},
|
||||
{"version", 1},
|
||||
{"fields", std::move(fields)}}});
|
||||
|
||||
return types;
|
||||
}
|
||||
} // 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,8 @@
|
||||
{
|
||||
"format": "faset.project",
|
||||
"version": 1,
|
||||
"id": "example-collect-2d-project",
|
||||
"name": "Collect & Escape 2D",
|
||||
"dimension": 2,
|
||||
"start_scene": "Scenes/main.scene.json"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
.faset/
|
||||
Exports/
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Recreate the demo's original exit arch with an unmodified Blender 4.5+.
|
||||
Run: blender --background --factory-startup --python create.py -- ENGINE_ROOT
|
||||
"""
|
||||
import pathlib
|
||||
import sys
|
||||
import uuid
|
||||
import bpy
|
||||
|
||||
root = pathlib.Path(sys.argv[sys.argv.index('--') + 1]).resolve()
|
||||
output = pathlib.Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(root / 'tools'))
|
||||
import blender_addon
|
||||
from blender_addon.bundle import publish_bundle
|
||||
|
||||
blender_addon.register()
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
material = bpy.data.materials.new('Weathered stone')
|
||||
material.diffuse_color = (0.22, 0.29, 0.34, 1)
|
||||
material.use_nodes = True
|
||||
shader = material.node_tree.nodes.get('Principled BSDF')
|
||||
shader.inputs['Base Color'].default_value = material.diffuse_color
|
||||
shader.inputs['Metallic'].default_value = 0.08
|
||||
shader.inputs['Roughness'].default_value = 0.8
|
||||
material['faset_id'] = '08046276-106a-4aeb-80f1-d4f02a2ffdc1'
|
||||
namespace = uuid.UUID('73023ab9-8fe6-4d56-b136-c25c51510b72')
|
||||
for name, position, size in [
|
||||
('Left post', (0, -1.3, 1.45), (0.45, 0.35, 2.9)),
|
||||
('Right post', (0, 1.3, 1.45), (0.45, 0.35, 2.9)),
|
||||
('Lintel', (0, 0, 3.0), (0.52, 3.0, 0.35)),
|
||||
]:
|
||||
bpy.ops.mesh.primitive_cube_add(location=position)
|
||||
obj = bpy.context.object
|
||||
obj.name = name
|
||||
obj.dimensions = size
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
obj.data.materials.append(material)
|
||||
obj['faset_id'] = str(uuid.uuid5(namespace, name))
|
||||
obj.data['faset_id'] = str(uuid.uuid5(namespace, name + ' mesh'))
|
||||
bevel = obj.modifiers.new('Small stone bevel', 'BEVEL')
|
||||
bevel.width = 0.06
|
||||
bevel.segments = 1
|
||||
bpy.context.scene['faset_asset_id'] = '5832763b-3ed0-44d6-9088-0b524f196a91'
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(output / 'source.blend'))
|
||||
raw = output / 'arch.glb'
|
||||
bpy.ops.export_scene.gltf(filepath=str(raw), export_format='GLB', export_extras=True,
|
||||
export_yup=True, export_animations=False, export_materials='EXPORT', use_active_scene=True)
|
||||
publish_bundle(raw, output, bpy.context.scene['faset_asset_id'], bpy.app.version_string, 'source.blend')
|
||||
raw.unlink()
|
||||
print('Faset exit arch recreated')
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"asset_id": "5832763b-3ed0-44d6-9088-0b524f196a91",
|
||||
"exporter": {
|
||||
"addon_version": "0.1.0",
|
||||
"blender_version": "4.5.3 LTS"
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"path": "payload/ee6c748a212905fc9c465cbe95c033e94b80be643624f4f240ad5353761205b3.glb",
|
||||
"sha256": "ee6c748a212905fc9c465cbe95c033e94b80be643624f4f240ad5353761205b3",
|
||||
"size": 5556
|
||||
}
|
||||
],
|
||||
"generation": "7000c0ddc0ef38a87a6bfa1833b757df648aecfbdc3c304036028ba7f530a121",
|
||||
"outputs": [
|
||||
{
|
||||
"kind": "node",
|
||||
"locator": "/nodes/0",
|
||||
"name": "Left post",
|
||||
"source_id": "4fbe8b04-1247-5088-a438-934d4e26c532"
|
||||
},
|
||||
{
|
||||
"kind": "node",
|
||||
"locator": "/nodes/1",
|
||||
"name": "Right post",
|
||||
"source_id": "3f82eddf-7169-590e-9116-77f91d809972"
|
||||
},
|
||||
{
|
||||
"kind": "node",
|
||||
"locator": "/nodes/2",
|
||||
"name": "Lintel",
|
||||
"source_id": "49b46a7c-9ae3-54c1-8e4f-ad4efe627330"
|
||||
},
|
||||
{
|
||||
"kind": "mesh",
|
||||
"locator": "/meshes/0",
|
||||
"name": "Cube.001",
|
||||
"source_id": "b359b954-1c3e-5ae9-a127-ce251b05193e"
|
||||
},
|
||||
{
|
||||
"kind": "mesh",
|
||||
"locator": "/meshes/1",
|
||||
"name": "Cube.002",
|
||||
"source_id": "0a83245a-e502-5fd8-978d-dd65931238a3"
|
||||
},
|
||||
{
|
||||
"kind": "mesh",
|
||||
"locator": "/meshes/2",
|
||||
"name": "Cube.003",
|
||||
"source_id": "6f6587d4-1d8e-56b5-9ec6-afbd6ac51e55"
|
||||
},
|
||||
{
|
||||
"kind": "material",
|
||||
"locator": "/materials/0",
|
||||
"name": "Weathered stone",
|
||||
"source_id": "08046276-106a-4aeb-80f1-d4f02a2ffdc1"
|
||||
}
|
||||
],
|
||||
"recipe": {
|
||||
"export_animations": false,
|
||||
"export_extras": true,
|
||||
"export_yup": true,
|
||||
"profile": "faset-gltf-static-v1"
|
||||
},
|
||||
"schema_version": 1,
|
||||
"source": {
|
||||
"path_hint": "source.blend"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"asset_id": "5832763b-3ed0-44d6-9088-0b524f196a91",
|
||||
"schema_version": 1,
|
||||
"settings": {}
|
||||
}
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
# Collect & Escape 3D
|
||||
|
||||
A small playable project using the real Faset C++ gameplay module and Box3D adapter. The exit arch is an original Blender asset included as a GLB bundle, source `.blend`, and reproducible Blender script. Importing the included bundle does not require Blender. Gameplay geometry and physics use built-in primitives.
|
||||
|
||||
## Play
|
||||
|
||||
Import the included Blender bundle once (repeat after clearing the project cache), then open the project from the repository root:
|
||||
|
||||
```bash
|
||||
build/linux-debug/faset_editor --project examples/projects/collect-3d \
|
||||
--command '{"name":"faset_import","arguments":{"path":"Assets/exit-arch/manifest.json"}}' --wait
|
||||
```
|
||||
|
||||
You can also import `Assets/exit-arch/manifest.json` from the Editor's Assets panel before Play.
|
||||
Open the Editor:
|
||||
|
||||
```bash
|
||||
build/linux-debug/faset_editor --project examples/projects/collect-3d
|
||||
```
|
||||
|
||||
Use **Play** to build this project's `Scripts/Gameplay.cpp` and start a separate Player. On Windows use your Windows build directory and `faset_editor.exe`. First complete the engine build setup in the User Manual.
|
||||
|
||||
For a direct Player build on Linux, from the repository root:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build/collect-3d -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DFASET_GAMEPLAY_SOURCE_DIR="$PWD/examples/projects/collect-3d/Scripts"
|
||||
cmake --build build/collect-3d --target faset_player faset_schema_exporter
|
||||
build/collect-3d/faset_player --scene examples/projects/collect-3d/Scenes/main.scene.json \
|
||||
--assets examples/projects/collect-3d/.faset/cache
|
||||
```
|
||||
|
||||
On Windows use a Developer shell with `clang-cl` and the Windows SDK, select `clang-cl` for both compiler options, and give `FASET_GAMEPLAY_SOURCE_DIR` an absolute path.
|
||||
|
||||
## Objective and controls
|
||||
|
||||
- WASD or arrow keys move on world X/Z. W points toward negative Z (away from the camera); diagonal speed is normalized.
|
||||
- **Space** jumps while a native contact supports the player block.
|
||||
- Collect all three **gold cubes**. Jump onto the raised platform for the middle pickup.
|
||||
- Each pickup moves to the progress display. After all three, the **red physical gate** moves out of the level.
|
||||
- Reach the **green exit** to reveal the large gold victory marker. Movement stops after completion.
|
||||
- **E** restores the player, pickups, gate, and victory state. Falling below the level also resets the round.
|
||||
- **P** pauses, **N** advances one fixed tick, and **Escape** closes the Player.
|
||||
|
||||
The Player window title and console identify the controls and objective; the game uses geometric progress markers rather than a text HUD. The player is a dynamic box with zero friction, not an articulated character controller. It can rotate after impacts.
|
||||
|
||||
## Inspect and change the game
|
||||
|
||||
`project.faset.json` chooses `Scenes/main.scene.json`. The scene stores object/component IDs, transforms, physics settings, and entity references in the collector component. `Scripts/Gameplay.cpp` owns the round's transient C++ state and registers its editable schema. `speed` and `jump_speed` use metres per second.
|
||||
|
||||
Edit the scene through the Inspector and restart Play to see new level data. Change C++ or its schema, then stop, build, and restart; C++ hot reload is not implemented. Runtime pickup progress does not rewrite the authoring scene or create Undo entries.
|
||||
|
||||
The project vendors `Scripts/Extensions/Beacon.hpp` from the engine's extension example. Its separate `example.beacon` schema and callback rotate the pickups and victory marker. This is a C++ header package explicitly registered by the game, not an Editor plugin or automatic reflection.
|
||||
|
||||
## Verify
|
||||
|
||||
The normal test build creates `faset_example_3d_tests`. It compiles this exact module, follows a route using movement/jump input, collects the elevated pickup, opens the gate, reaches the exit, resets the round, and restarts the scene. It does not teleport to pass the objective.
|
||||
|
||||
```bash
|
||||
cmake --build build/linux-debug --target faset_example_3d_tests
|
||||
ctest --test-dir build/linux-debug -R '^playable_3d$' --output-on-failure
|
||||
```
|
||||
|
||||
The runtime test is independent of graphics. A Player run additionally requires the supported Vulkan platform setup. The Editor's Export action creates a separate Release build and a cooked standalone generation; run that generation's Player from its export directory.
|
||||
|
||||
## Recreate the Blender asset
|
||||
|
||||
With an unmodified Blender 4.5 or later and the engine repository as the current directory:
|
||||
|
||||
```bash
|
||||
blender --background --factory-startup --python-exit-code 1 \
|
||||
--python examples/projects/collect-3d/Assets/exit-arch/create.py -- "$PWD"
|
||||
```
|
||||
|
||||
The script creates the three beveled stone pieces with persistent UUIDs, saves
|
||||
`source.blend`, and publishes a checksum-verified GLB manifest. Reimport the manifest
|
||||
after editing it. Placement and gameplay stay in the Faset scene. The arch is visual;
|
||||
the separate red gate supplies the gameplay collision.
|
||||
@@ -0,0 +1,845 @@
|
||||
{
|
||||
"format": "faset.scene",
|
||||
"version": 1,
|
||||
"id": "example-collect-3d-scene",
|
||||
"name": "Collect & Escape 3D \u2014 WASD / Space / E reset",
|
||||
"dimension": 3,
|
||||
"simulation": {
|
||||
"fixed_delta": 0.016666666666666666,
|
||||
"max_catch_up_ticks": 4,
|
||||
"physics_substeps": 4,
|
||||
"gravity": [
|
||||
0,
|
||||
-9.81,
|
||||
0
|
||||
]
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"id": "floor",
|
||||
"name": "Room floor",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "floor-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
-0.75,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
18,
|
||||
0.5,
|
||||
12
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "floor-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
0.2,
|
||||
0.3,
|
||||
0.39,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "floor-faset.rigid_body_3d",
|
||||
"type": "faset.rigid_body_3d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "platform",
|
||||
"name": "Jump platform",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "platform-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
-1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
2,
|
||||
1,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "platform-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
0.4,
|
||||
0.45,
|
||||
0.55,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "platform-faset.rigid_body_3d",
|
||||
"type": "faset.rigid_body_3d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "back-wall",
|
||||
"name": "Back wall",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "back-wall-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
-6
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
18,
|
||||
1,
|
||||
0.4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "back-wall-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
0.2,
|
||||
0.3,
|
||||
0.39,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "back-wall-faset.rigid_body_3d",
|
||||
"type": "faset.rigid_body_3d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "left-wall",
|
||||
"name": "Left wall",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "left-wall-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
-9,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
0.4,
|
||||
1,
|
||||
12
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "left-wall-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
0.2,
|
||||
0.3,
|
||||
0.39,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "left-wall-faset.rigid_body_3d",
|
||||
"type": "faset.rigid_body_3d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "right-wall",
|
||||
"name": "Right wall",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "right-wall-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
9,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
0.4,
|
||||
1,
|
||||
12
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "right-wall-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
0.2,
|
||||
0.3,
|
||||
0.39,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "right-wall-faset.rigid_body_3d",
|
||||
"type": "faset.rigid_body_3d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "front-wall",
|
||||
"name": "Front wall",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "front-wall-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
6
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
18,
|
||||
1,
|
||||
0.4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "front-wall-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
0.2,
|
||||
0.3,
|
||||
0.39,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "front-wall-faset.rigid_body_3d",
|
||||
"type": "faset.rigid_body_3d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "token_1",
|
||||
"name": "Gold pickup 1",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "token_1-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
-5,
|
||||
0.35,
|
||||
-3
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "token_1-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
1,
|
||||
0.68,
|
||||
0.14,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "token_1-example.beacon",
|
||||
"type": "example.beacon",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"speed": 1.0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "token_2",
|
||||
"name": "Gold pickup 2",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "token_2-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
-1,
|
||||
1.35,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "token_2-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
1,
|
||||
0.68,
|
||||
0.14,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "token_2-example.beacon",
|
||||
"type": "example.beacon",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"speed": 1.25
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "token_3",
|
||||
"name": "Gold pickup 3",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "token_3-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
3,
|
||||
0.35,
|
||||
3
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "token_3-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
1,
|
||||
0.68,
|
||||
0.14,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "token_3-example.beacon",
|
||||
"type": "example.beacon",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"speed": 1.5
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "gate",
|
||||
"name": "Collect all three to open",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "gate-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
6,
|
||||
1,
|
||||
-4
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
0.45,
|
||||
3,
|
||||
3
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gate-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
0.9,
|
||||
0.23,
|
||||
0.16,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gate-faset.rigid_body_3d",
|
||||
"type": "faset.rigid_body_3d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "static",
|
||||
"half_extents": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0.4,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "goal",
|
||||
"name": "Green exit",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "goal-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
7.5,
|
||||
0.15,
|
||||
-4
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
0.85,
|
||||
0.3,
|
||||
0.85
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "goal-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
0.15,
|
||||
0.9,
|
||||
0.47,
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "win_marker",
|
||||
"name": "Victory marker",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "win_marker-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
7.5,
|
||||
3.0,
|
||||
-4
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "win_marker-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
1,
|
||||
0.68,
|
||||
0.14,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "win_marker-example.beacon",
|
||||
"type": "example.beacon",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"speed": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "player",
|
||||
"name": "Blue player block",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "player-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
-6,
|
||||
0,
|
||||
3
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "player-faset.mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "builtin:cube",
|
||||
"color": [
|
||||
0.12,
|
||||
0.65,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "player-faset.rigid_body_3d",
|
||||
"type": "faset.rigid_body_3d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"body_type": "dynamic",
|
||||
"half_extents": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"density": 1,
|
||||
"friction": 0,
|
||||
"restitution": 0,
|
||||
"gravity_scale": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "player-example.collector_3d",
|
||||
"type": "example.collector_3d",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"speed": 4,
|
||||
"jump_speed": 6,
|
||||
"gate": "gate",
|
||||
"goal": "goal",
|
||||
"win_marker": "win_marker",
|
||||
"token_1": "token_1",
|
||||
"token_2": "token_2",
|
||||
"token_3": "token_3"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "camera",
|
||||
"name": "Room camera",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "camera-faset.transform",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
11,
|
||||
13,
|
||||
18
|
||||
],
|
||||
"rotation": [
|
||||
-0.5522890193034389,
|
||||
0.5485494024505281,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "camera-faset.camera",
|
||||
"type": "faset.camera",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"fov": 50,
|
||||
"near": 0.1,
|
||||
"far": 200
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "blender-exit-arch",
|
||||
"name": "Blender exit arch",
|
||||
"parent": null,
|
||||
"components": [
|
||||
{
|
||||
"id": "blender-exit-arch-pose",
|
||||
"type": "faset.transform",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"position": [
|
||||
7.4,
|
||||
-0.5,
|
||||
-4
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "blender-exit-arch-mesh",
|
||||
"type": "faset.mesh",
|
||||
"version": 1,
|
||||
"fields": {
|
||||
"asset": "5832763b-3ed0-44d6-9088-0b524f196a91",
|
||||
"primitive": "asset",
|
||||
"color": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"instances": []
|
||||
}
|
||||
@@ -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,157 @@
|
||||
#include "Gameplay.hpp"
|
||||
#include "Extensions/Beacon.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
|
||||
namespace faset::gameplay {
|
||||
namespace {
|
||||
constexpr bool is3D = true;
|
||||
constexpr const char* collectorType = "example.collector_3d";
|
||||
using Key = std::tuple<std::uint64_t, std::uint32_t, std::uint64_t>;
|
||||
Key key(runtime::EntityHandle handle) {
|
||||
return {handle.session, handle.slot, handle.generation};
|
||||
}
|
||||
struct Round {
|
||||
runtime::Transform spawn, gatePose, victoryPose;
|
||||
runtime::EntityHandle gate, goal, victory;
|
||||
std::array<runtime::EntityHandle, 3> tokens;
|
||||
std::array<runtime::Transform, 3> tokenPoses;
|
||||
std::array<bool, 3> collected{};
|
||||
bool won{};
|
||||
bool gateOpened{};
|
||||
};
|
||||
float distance(runtime::Vec3 a, runtime::Vec3 b) {
|
||||
const float x = a[0] - b[0], y = a[1] - b[1], z = is3D ? a[2] - b[2] : 0;
|
||||
return std::sqrt(x * x + y * y + z * z);
|
||||
}
|
||||
void reset(runtime::Runtime& game, runtime::EntityHandle self, Round& round) {
|
||||
game.teleport(self, round.spawn);
|
||||
game.setVelocity(self, {0, 0, 0});
|
||||
game.teleport(round.gate, round.gatePose);
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
game.setTransform(round.tokens[i], round.tokenPoses[i]);
|
||||
auto hidden = round.victoryPose;
|
||||
hidden.position[1] = -50;
|
||||
game.setTransform(round.victory, hidden);
|
||||
round.collected = {};
|
||||
round.won = false;
|
||||
round.gateOpened = false;
|
||||
std::cout << "New round: collect the three gold cubes, then reach the green exit. E resets.\n";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void registerGameplay(runtime::Runtime& world) {
|
||||
beacon::register_behavior(world);
|
||||
auto rounds = std::make_shared<std::map<Key, Round>>();
|
||||
runtime::Behavior collector;
|
||||
collector.onStart = [rounds](runtime::Runtime& game, runtime::EntityHandle self, double) {
|
||||
const auto fields = game.fields(self, collectorType);
|
||||
Round round;
|
||||
round.spawn = game.transform(self);
|
||||
auto resolve = [&](const char* field) {
|
||||
const auto handle = game.find(fields.at(field).get<std::string>());
|
||||
if (!game.valid(handle))
|
||||
throw std::runtime_error(std::string("Missing level reference: ") + field);
|
||||
return handle;
|
||||
};
|
||||
round.gate = resolve("gate");
|
||||
round.goal = resolve("goal");
|
||||
round.victory = resolve("win_marker");
|
||||
round.gatePose = game.transform(round.gate);
|
||||
round.victoryPose = game.transform(round.victory);
|
||||
for (std::size_t i = 0; i < 3; ++i) {
|
||||
const auto field = "token_" + std::to_string(i + 1);
|
||||
round.tokens[i] = resolve(field.c_str());
|
||||
round.tokenPoses[i] = game.transform(round.tokens[i]);
|
||||
}
|
||||
auto& stored = rounds->insert_or_assign(key(self), std::move(round)).first->second;
|
||||
reset(game, self, stored);
|
||||
};
|
||||
collector.onDestroy = [rounds](runtime::Runtime&, runtime::EntityHandle self, double) {
|
||||
rounds->erase(key(self));
|
||||
};
|
||||
collector.fixedUpdate = [rounds](runtime::Runtime& game, runtime::EntityHandle self, double) {
|
||||
auto& round = rounds->at(key(self));
|
||||
const auto input = game.input();
|
||||
const auto position = game.transform(self).position;
|
||||
if (input.interactPressed || position[1] < -8) {
|
||||
reset(game, self, round);
|
||||
return;
|
||||
}
|
||||
const auto fields = game.fields(self, collectorType);
|
||||
auto velocity = game.velocity(self);
|
||||
float x = input.horizontal, z = is3D ? -input.vertical : 0;
|
||||
const float length = std::sqrt(x * x + z * z);
|
||||
if (length > 1) {
|
||||
x /= length;
|
||||
z /= length;
|
||||
}
|
||||
const auto speed = fields.value("speed", 4.0f);
|
||||
velocity[0] = round.won ? 0 : x * speed;
|
||||
if (is3D)
|
||||
velocity[2] = round.won ? 0 : z * speed;
|
||||
if (!round.won && input.jumpPressed && game.grounded(self))
|
||||
velocity[1] = fields.value("jump_speed", 6.0f);
|
||||
game.setVelocity(self, velocity);
|
||||
if (round.won)
|
||||
return;
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
if (!round.collected[i] && distance(position, round.tokenPoses[i].position) < 0.95f) {
|
||||
round.collected[i] = true;
|
||||
auto display = round.tokenPoses[i];
|
||||
display.position = is3D ? runtime::Vec3{6, 2 + float(i) * 0.8f, -4}
|
||||
: runtime::Vec3{-1.2f + float(i) * 1.2f, 4.5f, 0};
|
||||
game.setTransform(round.tokens[i], display);
|
||||
std::cout << "Collected "
|
||||
<< std::count(round.collected.begin(), round.collected.end(), true)
|
||||
<< "/3\n";
|
||||
}
|
||||
if (std::all_of(round.collected.begin(), round.collected.end(),
|
||||
[](bool value) { return value; })) {
|
||||
if (!round.gateOpened) {
|
||||
auto open = round.gatePose;
|
||||
open.position[1] = -30;
|
||||
game.teleport(round.gate, open);
|
||||
round.gateOpened = true;
|
||||
}
|
||||
if (distance(position, game.transform(round.goal).position) < 1.0f) {
|
||||
round.won = true;
|
||||
game.setTransform(round.victory, round.victoryPose);
|
||||
std::cout << "Level complete! The gold victory marker is visible. Press E to play "
|
||||
"again.\n";
|
||||
}
|
||||
}
|
||||
};
|
||||
world.registerBehavior(collectorType, std::move(collector));
|
||||
}
|
||||
|
||||
nlohmann::json schema() {
|
||||
nlohmann::json fields = {{"speed",
|
||||
{{"id", "speed"},
|
||||
{"name", "Move speed"},
|
||||
{"type", "number"},
|
||||
{"default", 4.0},
|
||||
{"min", 0.0},
|
||||
{"units", "m/s"}}},
|
||||
{"jump_speed",
|
||||
{{"id", "jump_speed"},
|
||||
{"name", "Jump speed"},
|
||||
{"type", "number"},
|
||||
{"default", 6.0},
|
||||
{"min", 0.0},
|
||||
{"units", "m/s"}}}};
|
||||
for (const auto* id : {"gate", "goal", "win_marker", "token_1", "token_2", "token_3"})
|
||||
fields[id] = {{"id", id}, {"name", id}, {"type", "entity_ref"}, {"default", id}};
|
||||
auto types = nlohmann::json::array({{{"id", collectorType},
|
||||
{"name", "Collect three and escape"},
|
||||
{"version", 1},
|
||||
{"fields", std::move(fields)}}});
|
||||
types.push_back(beacon::schema());
|
||||
return types;
|
||||
}
|
||||
} // 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,8 @@
|
||||
{
|
||||
"format": "faset.project",
|
||||
"version": 1,
|
||||
"id": "example-collect-3d-project",
|
||||
"name": "Collect & Escape 3D",
|
||||
"dimension": 3,
|
||||
"start_scene": "Scenes/main.scene.json"
|
||||
}
|
||||
Reference in New Issue
Block a user