Created
July 29, 2020 19:10
-
-
Save nikki93/a7d55d799d28519e4138da97a33b6313 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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