Skip to content

Instantly share code, notes, and snippets.

@nikki93
Created July 29, 2020 19:10
Show Gist options
  • Save nikki93/a7d55d799d28519e4138da97a33b6313 to your computer and use it in GitHub Desktop.
Save nikki93/a7d55d799d28519e4138da97a33b6313 to your computer and use it in GitHub Desktop.
#include "precomp.h"
#include "events.h"
#include "graphics.h"
#include "kernel.h"
#include "physics.h"
#include "platform.h"
#include "timing.h"
#include "ui.h"
//
// Components
//
// Basics
struct Position {
double x = 0, y = 0;
};
struct Sprite {
Graphics::Image image;
double scale = 1;
double depth = 0;
};
// Physics
struct Feet {
Physics::Body body;
Physics::Shape shape;
};
struct Walk {
Physics::Body target;
Physics::Constraint constraint;
Walk(Physics &phy, Physics::Body &body)
: target(phy.createStatic().setPosition(body.getPosition()))
, constraint(
phy.createPivot(target, body, { 0, 0 }, { 0, 0 }).setMaxForce(3000).setMaxBias(200)) {
}
};
struct Friction {
Physics::Constraint constraint;
Friction(Physics &phy, Physics::Body &body)
: constraint(phy.createPivot(phy.getBackground(), body, { 0, 0 }, { 0, 0 })
.setMaxForce(800)
.setMaxBias(0)) {
}
};
// Edit
struct Select {};
struct EditFeetShape {};
// Tags
struct Player {};
struct Prop {};
//
// Triggers
//
// Physics
struct PhysicsInput : Kernel::Trigger<Physics &, const Events &> {};
struct PhysicsPost : Kernel::Trigger<Physics &> {};
// Draw
struct Draw : Kernel::Trigger<Graphics &> {};
// Edit
struct EditInput : Kernel::Trigger<const Events &, Physics &> {};
struct EditVerify : Kernel::Trigger<bool &> {};
struct EditDraw : Kernel::Trigger<Graphics &> {};
struct EditSideUI : Kernel::Trigger<UI &, Physics &, bool &> {};
//
// Rules
//
// Sprite
RULE(Draw, DrawSprites)(Kernel &ker, Graphics &gfx) {
// Order sprites by depth and draw at positions
ker.isort<Sprite>([](const Sprite &a, const Sprite &b) {
return a.depth < b.depth;
});
ker.view<Sprite, Position>().each([&](const Sprite &spr, const Position &pos) {
gfx.drawImage(spr.image, pos.x, pos.y, spr.scale);
});
};
// Player
RULE(PhysicsInput, WalkPlayerToTouch)(Kernel &ker, Physics &phy, const Events &ev) {
auto &touches = ev.getTouches();
if (touches.size() > 0) {
// Touching: add walk, set target to first touch
ker.view<Player, Feet>(Kernel::exclude<Walk>).each([&](const Entity ent, Feet &feet) {
ker.add<Walk>(ent, phy, feet.body);
});
ker.view<Player, Walk>().each([&](Walk &walk) {
walk.target.setPosition({ touches[0].x, touches[0].y });
});
} else {
// Not touching: remove walk
ker.view<Player, Walk>().each([&](const Entity ent, Walk &) {
ker.remove<Walk>(ent);
});
}
};
RULE(PhysicsPost, ReadPlayerPhysics)(Kernel &ker, Physics &) {
// Read physics to position
ker.view<Player, Feet, Position>().each([&](const Feet &feet, Position &pos) {
auto [x, y] = feet.body.getPosition();
pos.x = x;
pos.y = y - 65;
});
};
RULE(PhysicsPost, UpdatePlayerDepth)(Kernel &ker, Physics &phy) {
// Set player depth behind objects that obscure it
ker.view<Player, Feet, Sprite>().each([&](const Entity ent, const Feet &feet, Sprite &spr) {
spr.depth = 1000;
const auto query = [&](double x, double y) {
phy.segmentQuery({ x, y }, { x, y + 1e4 }, 1, [&](Physics::Shape &shape, Vec2, Vec2, double) {
auto other = shape.getBody().getEntity();
if (other != ent && other != Kernel::null && ker.has<Sprite>(other)) {
spr.depth = std::min(spr.depth, ker.get<Sprite>(other).depth - 0.2);
}
});
};
auto [x, y] = feet.body.getPosition();
query(x + 22, y);
query(x - 22, y);
query(x, y);
});
};
// Edit
RULE(EditInput, EditTouch)(Kernel &ker, const Events &ev, Physics &phy) {
auto &touches = ev.getTouches();
// Editing a feet shape?
if (auto view = ker.view<EditFeetShape, Feet>(); !view.empty()) {
// Remove vertices at touch press, add vertex at touch release
if (touches.size() == 1 && (touches[0].pressed || touches[0].released)) {
view.each([&](Feet &feet) {
std::vector<Vec2> verts;
for (auto nVerts = feet.shape.getNumVertices(), i = 0; i < nVerts; ++i) {
auto v = feet.shape.getVertex(i);
auto [wx, wy] = feet.body.toWorld(v);
if (!(abs(touches[0].x - wx) < 2 && abs(touches[0].y - wy) < 2)) {
verts.push_back(v);
}
}
if (touches[0].released || verts.size() == 0) {
verts.push_back(feet.body.toLocal({ touches[0].x, touches[0].y }));
}
feet.shape = phy.createPoly(feet.body, verts);
});
}
return;
}
// Single touch to select
if (touches.size() == 1 && touches[0].pressed) {
// Collect hits in ascending area order
std::vector<std::pair<double, Entity>> hits;
ker.view<Sprite, Position>().each(
[&](const Entity ent, const Sprite &spr, const Position &pos) {
auto [imgW, imgH] = spr.image.getSize();
auto w = spr.scale * imgW, h = spr.scale * imgH;
if (abs(touches[0].x - pos.x) < 0.5 * w && abs(touches[0].y - pos.y) < 0.5 * h) {
hits.emplace_back(w * h, ent);
}
});
std::sort(hits.begin(), hits.end());
// Pick after current selection or first if none
auto pick = Kernel::null;
auto pickNext = true;
for (const auto &[order, ent] : hits) {
if (ker.has<Select>(ent)) {
pickNext = true;
} else if (pickNext) {
pick = ent;
pickNext = false;
}
}
ker.clear<Select>();
if (pick != Kernel::null) {
ker.add<Select>(pick);
}
}
};
RULE(EditVerify, Deselect)(Kernel &ker, bool &editing) {
// Remove edit components on deselect or leaving edit
ker.view<EditFeetShape>(Kernel::exclude<Select>).each([&](const Entity ent) {
ker.remove<EditFeetShape>(ent);
});
if (!editing) {
ker.clear<EditFeetShape>();
}
};
RULE(EditDraw, DrawBoxes)(Kernel &ker, Graphics &gfx) {
// Editing a feet shape?
if (auto view = ker.view<EditFeetShape, Feet>(); !view.empty()) {
gfx.setColor(0, 0, 0xff);
view.each([&](const Feet &feet) {
for (auto nVerts = feet.shape.getNumVertices(), i = 0; i < nVerts; ++i) {
auto [wx, wy] = feet.body.toWorld(feet.shape.getVertex(i));
gfx.drawRectangleFill(wx, wy, 4, 4);
}
feet.body.draw(gfx);
});
return;
}
// Feet shapes
gfx.scope([&]() {
gfx.setColor(0, 0, 0xff);
ker.view<Feet>().each([&](const Feet &feet) {
feet.body.draw(gfx);
});
});
// Thin red boxes for unselected
gfx.scope([&]() {
gfx.setColor(0xff, 0, 0);
ker.view<Sprite, Position>(Kernel::exclude<Select>)
.each([&](const Sprite &spr, const Position &pos) {
auto [imgW, imgH] = spr.image.getSize();
gfx.drawRectangle(pos.x, pos.y, spr.scale * imgW, spr.scale * imgH);
});
});
// Thick doubled green boxes for selected. Draw at view boundary if covers view.
double vw = 0, vh = 0;
std::tie(vw, vh) = gfx.getViewSize();
gfx.scope([&]() {
gfx.setColor(0, 0x80, 0x40);
ker.view<Select, Sprite, Position>().each([&](const Sprite &spr, const Position &pos) {
auto x = pos.x, y = pos.y;
auto [imgW, imgH] = spr.image.getSize();
auto w = spr.scale * imgW, h = spr.scale * imgH;
if (x - w <= 0 && x + w >= vw && y - h <= 0 && y + h >= vh) {
x = 0.5 * vw;
y = 0.5 * vh;
w = vw - 4;
h = vh - 4;
}
gfx.drawRectangle(x, y, w, h);
gfx.drawRectangle(x, y, w + 4, h + 4);
});
});
};
RULE(EditSideUI, Inspector)(Kernel &ker, UI &ui, Physics &phy, bool &editing) {
// Inspector for selected
ker.view<Select>().each([&](const Entity ent) {
ui.div()("inspector")([&]() {
std::vector<std::function<void(void)>> after;
const auto section = [&](const char *title, auto &&f) {
ui.key(title).elem("details")(title)("open", true)([&]() {
auto remove = false;
ui.elem("summary")([&]() {
ui.text(title);
if constexpr (std::is_invocable_v<decltype(f), bool>) {
ui.button()("remove")("click", [&](emscripten::val) {
remove = true;
});
}
});
if constexpr (std::is_invocable_v<decltype(f), bool>) {
f(remove);
} else {
f();
}
});
};
if (ker.has<Position>(ent)) {
auto &pos = ker.get<Position>(ent);
section("position", [&]() {
ui.div()("info")([&]() {
ui.text("x: {:.2f}, y: {:.2f}", pos.x, pos.y);
});
});
}
if (ker.has<Sprite>(ent)) {
auto &spr = ker.get<Sprite>(ent);
section("sprite", [&]() {
if (static auto firstFrame = true; !firstFrame) { // Workaround for Safari
ui.elem("img")("preview")("src", spr.image.getBlobUrl());
} else {
firstFrame = false;
}
ui.div()("info")([&]() {
ui.text("path: {}", spr.image.getPath());
});
ui.div()("info")([&]() {
auto [imgW, imgH] = spr.image.getSize();
ui.text("width: {}, height: {}", imgW, imgH);
});
ui.div()("info")([&]() {
ui.text("scale: {}", spr.scale);
});
ui.div()("info")([&]() {
ui.text("depth: {}", spr.depth);
});
});
}
if (ker.has<Feet>(ent)) {
auto &feet = ker.get<Feet>(ent);
section("feet", [&](bool remove) {
if (remove && !ker.has<Player>(ent)) {
after.emplace_back([&]() {
ker.remove<Feet>(ent);
});
return;
}
ui.div()("info")([&]() {
ui.text("shape: {} vertices", feet.shape.getNumVertices());
if (!ker.has<Player>(ent)) {
ui.button()("shape")("selected", ker.has<EditFeetShape>(ent))(
"click", [&](emscripten::val) {
editing = true;
if (ker.has<EditFeetShape>(ent)) {
ker.remove<EditFeetShape>(ent);
} else {
ker.add<EditFeetShape>(ent);
}
});
}
});
});
}
if (ker.has<Player>(ent)) {
section("player", [&]() {
});
}
if (ker.has<Prop>(ent)) {
section("prop", [&]() {
});
}
ui.div()("add-bar")([&]() {
if (ker.has<Position>(ent) && !ker.has<Feet>(ent)) {
ui.button()("add")("label", "feet")("click", [&](emscripten::val) {
const auto &pos = ker.get<Position>(ent);
auto body = phy.createStatic().setEntity(ent).setPosition({ pos.x, pos.y });
auto shape = phy.createBox(body, 40, 40);
ker.add<Feet>(ent, std::move(body), std::move(shape));
});
}
});
for (auto &func : after) {
func();
}
});
});
};
//
// Scene
//
namespace Scene {
auto load(Kernel &ker, Graphics &gfx, const std::string &path, double scale = 0.5) -> void {
std::ifstream ifs(path);
rapidjson::BasicIStreamWrapper isw(ifs);
rapidjson::Document root;
root.ParseStream(isw);
for (auto &[sectionName, section] : root.GetObject()) {
if (sectionName == "objects") {
auto depth = 0.0;
for (auto &object : section.GetArray()) {
auto x = object["x"].GetDouble();
auto y = object["y"].GetDouble();
auto imagePath = Platform::getAssetPath(object["imageName"].GetString());
auto &type = object["type"];
if (type == "prop") {
auto ent = ker.create();
ker.add<Prop>(ent);
auto &spr = ker.add<Sprite>(ent, gfx.createImage(imagePath), scale, depth++);
auto [imgW, imgH] = spr.image.getSize();
ker.add<Position>(ent, scale * (x + 0.5 * imgW), scale * (y + 0.5 * imgH));
}
}
}
}
}
auto createPlayer(Kernel &ker, Graphics &gfx, Physics &phy, double x, double y) -> void {
auto ent = ker.create();
ker.add<Player>(ent);
ker.add<Sprite>(ent, gfx.createImage(Platform::getAssetPath("player.png")), 0.25, 1000.0);
auto &pos = ker.add<Position>(ent, x, y);
auto body = phy.createDynamic(1, INFINITY).setEntity(ent).setPosition({ pos.x, pos.y });
auto shape = phy.createBox(body, 45, 20);
auto &feet = ker.add<Feet>(ent, std::move(body), std::move(shape));
ker.add<Friction>(ent, phy, feet.body);
}
}
//
// main
//
auto main() -> int {
// Systems
Timing tim;
Graphics gfx("dream hotel");
UI ui(tim);
Events ev(gfx);
Physics phy(tim);
Kernel ker(tim);
// Editing
auto editing = false;
// Scene
Scene::load(ker, gfx, Platform::getAssetPath("test.scn"));
Scene::createPlayer(ker, gfx, phy, 120, 120);
// Loop
ev.loop([&]() {
// Timing
tim.frame();
// Unfocused?
if (!ev.isWindowFocused()) {
return;
}
// Logic
if (editing) {
// Edit
ker.run<EditInput>(ev, phy);
ker.run<EditVerify>(editing);
} else {
// Physics
ker.run<PhysicsInput>(phy, ev);
phy.frame();
ker.run<PhysicsPost>(phy);
}
// Graphics
gfx.frame([&]() {
// Background color
gfx.clear(0xcc, 0xe4, 0xf5);
// Draw
ker.run<Draw>(gfx);
if (editing) {
ker.run<EditDraw>(gfx);
}
});
// UI
ui.frame([&]() {
// Top
ui.panel("top", [&]() {
// Toolbar
ui.div()("toolbar")([&]() {
ui.button()("edit")("selected", editing)("click", [&](emscripten::val) {
editing = !editing;
ker.run<EditVerify>(editing);
});
});
});
// Bottom
ui.panel("bottom", [&]() {
// Status
ui.div()("status")([&]() {
// Reload
ui.button()("reload")("click", [&](emscripten::val) {
emscripten::val::global("location").call<void>("reload");
});
// FPS
ui.div()("fps")([&]() {
ui.text("fps: {}", int(round(tim.getFPS())));
});
});
});
// Side
ui.panel("side", [&]() {
// Edit
ker.run<EditSideUI>(ui, phy, editing);
});
});
});
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment