rngine 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/nitrogen/generated/shared/json/GameEngineConfig.json +9 -0
- package/package.json +3 -3
- package/shared/ColorUtils.hpp +25 -0
- package/shared/GameLoop.cpp +161 -0
- package/shared/GameLoop.hpp +53 -0
- package/shared/GameMethods.cpp +137 -0
- package/shared/GameMethods.hpp +20 -0
- package/shared/GameStats.hpp +11 -0
- package/shared/Rect.hpp +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rngine",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "React native library for building 2d games",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"lib",
|
|
18
18
|
"android",
|
|
19
19
|
"ios",
|
|
20
|
-
"
|
|
20
|
+
"shared",
|
|
21
21
|
"nitrogen",
|
|
22
22
|
"nitro.json",
|
|
23
23
|
"*.podspec",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"scripts": {
|
|
37
37
|
"example": "yarn workspace rngine-example",
|
|
38
38
|
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
39
|
-
"prepare": "bob build",
|
|
39
|
+
"prepare": "bob build && node scripts/nitrogen-patch.js",
|
|
40
40
|
"nitrogen": "nitrogen",
|
|
41
41
|
"typecheck": "tsc",
|
|
42
42
|
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include <stdexcept>
|
|
3
|
+
#include <string>
|
|
4
|
+
|
|
5
|
+
namespace margelo::nitro::rngine {
|
|
6
|
+
|
|
7
|
+
inline uint32_t parseHexColor(const std::string &color) {
|
|
8
|
+
if (color.empty() || color[0] != '#') {
|
|
9
|
+
throw std::invalid_argument("Only hex colors supported");
|
|
10
|
+
}
|
|
11
|
+
std::string hex = color.substr(1);
|
|
12
|
+
if (hex.size() == 6) {
|
|
13
|
+
return 0xFF000000 | std::stoul(hex, nullptr, 16);
|
|
14
|
+
} else if (hex.size() == 8) {
|
|
15
|
+
uint32_t rgba = std::stoul(hex, nullptr, 16);
|
|
16
|
+
uint32_t r = (rgba >> 24) & 0xFF;
|
|
17
|
+
uint32_t g = (rgba >> 16) & 0xFF;
|
|
18
|
+
uint32_t b = (rgba >> 8) & 0xFF;
|
|
19
|
+
uint32_t a = rgba & 0xFF;
|
|
20
|
+
return (a << 24) | (r << 16) | (g << 8) | b;
|
|
21
|
+
}
|
|
22
|
+
throw std::invalid_argument("Invalid hex color: " + color);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
} // namespace margelo::nitro::rngine
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#include "GameLoop.hpp"
|
|
2
|
+
#include "ColorUtils.hpp"
|
|
3
|
+
#include "Entity.hpp"
|
|
4
|
+
#include <android/log.h>
|
|
5
|
+
#include <chrono>
|
|
6
|
+
#include <cinttypes>
|
|
7
|
+
|
|
8
|
+
namespace margelo::nitro::rngine {
|
|
9
|
+
|
|
10
|
+
GameLoop &GameLoop::getInstance() {
|
|
11
|
+
static GameLoop instance;
|
|
12
|
+
return instance;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
GameLoop::GameLoop() {
|
|
16
|
+
__android_log_print(ANDROID_LOG_INFO, "GameLoop",
|
|
17
|
+
"Constructor - Starting game thread");
|
|
18
|
+
_gameThread = std::make_unique<std::thread>(&GameLoop::runGameLoop, this);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
GameLoop::~GameLoop() {
|
|
22
|
+
__android_log_print(ANDROID_LOG_INFO, "GameLoop",
|
|
23
|
+
"Destructor - Stopping game thread");
|
|
24
|
+
|
|
25
|
+
_isRunning.store(false);
|
|
26
|
+
|
|
27
|
+
if (_gameThread && _gameThread->joinable()) {
|
|
28
|
+
_gameThread->join();
|
|
29
|
+
}
|
|
30
|
+
__android_log_print(ANDROID_LOG_INFO, "GameLoop", "Destructor - Complete");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
std::vector<Entity *>
|
|
34
|
+
GameLoop::resolveEntitiesInternal(const std::string &prefix) {
|
|
35
|
+
std::vector<Entity *> results;
|
|
36
|
+
|
|
37
|
+
auto exact = _entities.find(prefix);
|
|
38
|
+
if (exact != _entities.end()) {
|
|
39
|
+
results.push_back(&exact->second);
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
std::string pattern = prefix + "_";
|
|
44
|
+
for (auto it = _entities.lower_bound(pattern); it != _entities.end(); ++it) {
|
|
45
|
+
if (it->first.rfind(pattern, 0) != 0)
|
|
46
|
+
break;
|
|
47
|
+
results.push_back(&it->second);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
std::vector<Rect> GameLoop::getRectsSnapshot() {
|
|
54
|
+
std::lock_guard<std::mutex> lock(_mutex);
|
|
55
|
+
std::vector<Rect> rects;
|
|
56
|
+
rects.reserve(_entities.size() + 1);
|
|
57
|
+
|
|
58
|
+
rects.push_back(
|
|
59
|
+
{0, _world.width, 0, _world.height, parseHexColor(_world.color)});
|
|
60
|
+
|
|
61
|
+
for (const auto &[id, entity] : _entities) {
|
|
62
|
+
if (entity.px + entity.width < 0 || entity.px > _world.width ||
|
|
63
|
+
entity.py + entity.height < 0 || entity.py > _world.height) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
rects.push_back(
|
|
68
|
+
{std::clamp(entity.px, 0.0, _world.width),
|
|
69
|
+
std::clamp(entity.px + entity.width, 0.0, _world.width),
|
|
70
|
+
std::clamp(entity.py, 0.0, _world.height),
|
|
71
|
+
std::clamp(entity.py + entity.height, 0.0, _world.height),
|
|
72
|
+
parseHexColor(entity.color)});
|
|
73
|
+
}
|
|
74
|
+
return rects;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
void GameLoop::runGameLoop() {
|
|
78
|
+
using namespace std::chrono;
|
|
79
|
+
double targetDeltaTime = 1.0 / _tickRate;
|
|
80
|
+
|
|
81
|
+
__android_log_print(ANDROID_LOG_INFO, "GameLoop", "Game loop thread started");
|
|
82
|
+
|
|
83
|
+
auto previousTime = steady_clock::now();
|
|
84
|
+
double accumulator = 0.0;
|
|
85
|
+
|
|
86
|
+
while (_isRunning) {
|
|
87
|
+
auto currentTime = steady_clock::now();
|
|
88
|
+
double frameTime = duration<double>(currentTime - previousTime).count();
|
|
89
|
+
previousTime = currentTime;
|
|
90
|
+
|
|
91
|
+
if (frameTime > 0.25) {
|
|
92
|
+
frameTime = 0.25;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!_isPaused) {
|
|
96
|
+
accumulator += frameTime;
|
|
97
|
+
|
|
98
|
+
while (accumulator >= targetDeltaTime) {
|
|
99
|
+
update(targetDeltaTime);
|
|
100
|
+
accumulator -= targetDeltaTime;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
std::this_thread::sleep_for(milliseconds(1));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
__android_log_print(ANDROID_LOG_INFO, "GameLoop", "Game loop thread stopped");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
void GameLoop::runSystems() {
|
|
111
|
+
for (auto &system : _systems) {
|
|
112
|
+
std::vector<Entity> entities;
|
|
113
|
+
|
|
114
|
+
for (const auto &id : system.ids) {
|
|
115
|
+
auto resolvedEntitiesInternal = resolveEntitiesInternal(id);
|
|
116
|
+
for (auto *entity : resolvedEntitiesInternal)
|
|
117
|
+
entities.push_back(*entity);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
system.onTick(entities);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
void GameLoop::update(double deltaTime) {
|
|
125
|
+
updateStats(deltaTime);
|
|
126
|
+
updateEntities(deltaTime);
|
|
127
|
+
runSystems();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
void GameLoop::updateStats(double deltaTime) {
|
|
131
|
+
static double timeAccumulator = 0.0;
|
|
132
|
+
static uint64_t frameCount = 0;
|
|
133
|
+
|
|
134
|
+
timeAccumulator += deltaTime;
|
|
135
|
+
frameCount++;
|
|
136
|
+
|
|
137
|
+
if (timeAccumulator >= 1.0) {
|
|
138
|
+
_gameStats.tickRate = static_cast<double>(frameCount);
|
|
139
|
+
_gameStats.deltaTime = deltaTime;
|
|
140
|
+
|
|
141
|
+
__android_log_print(ANDROID_LOG_DEBUG, "GameLoop",
|
|
142
|
+
"TickRate: %.2f, Delta: %.4f, Total Ticks: %" PRIu64,
|
|
143
|
+
_gameStats.tickRate, _gameStats.deltaTime,
|
|
144
|
+
_gameStats.totalTicks);
|
|
145
|
+
|
|
146
|
+
timeAccumulator = 0.0;
|
|
147
|
+
frameCount = 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_gameStats.totalTicks++;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
void GameLoop::updateEntities(double deltaTime) {
|
|
154
|
+
std::lock_guard<std::mutex> lock(_mutex);
|
|
155
|
+
|
|
156
|
+
for (auto &[id, entity] : _entities) {
|
|
157
|
+
entity.px += entity.vx * deltaTime;
|
|
158
|
+
entity.py += entity.vy * deltaTime;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} // namespace margelo::nitro::rngine
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "Entity.hpp"
|
|
4
|
+
#include "GameStats.hpp"
|
|
5
|
+
#include "Rect.hpp"
|
|
6
|
+
#include "System.hpp"
|
|
7
|
+
#include "World.hpp"
|
|
8
|
+
#include <atomic>
|
|
9
|
+
#include <memory>
|
|
10
|
+
#include <thread>
|
|
11
|
+
|
|
12
|
+
namespace margelo::nitro::rngine {
|
|
13
|
+
class GameLoop {
|
|
14
|
+
public:
|
|
15
|
+
static GameLoop &getInstance();
|
|
16
|
+
|
|
17
|
+
GameLoop(const GameLoop &) = delete;
|
|
18
|
+
GameLoop &operator=(const GameLoop &) = delete;
|
|
19
|
+
GameLoop(GameLoop &&) = delete;
|
|
20
|
+
GameLoop &operator=(GameLoop &&) = delete;
|
|
21
|
+
|
|
22
|
+
~GameLoop();
|
|
23
|
+
|
|
24
|
+
std::mutex &getMutexInternal() { return _mutex; };
|
|
25
|
+
std::map<string, Entity> &getEntitiesInternal() { return _entities; };
|
|
26
|
+
std::vector<System> &getSystemsInternal() { return _systems; };
|
|
27
|
+
std::atomic<bool> &getIsPausedInternal() { return _isPaused; };
|
|
28
|
+
World &getWorldInternal() { return _world; };
|
|
29
|
+
|
|
30
|
+
std::vector<Entity *> resolveEntitiesInternal(const std::string &prefix);
|
|
31
|
+
std::vector<Rect> getRectsSnapshot();
|
|
32
|
+
|
|
33
|
+
void setTickRate(double tickRate) { _tickRate = tickRate; };
|
|
34
|
+
|
|
35
|
+
private:
|
|
36
|
+
explicit GameLoop();
|
|
37
|
+
std::mutex _mutex;
|
|
38
|
+
std::map<std::string, Entity> _entities;
|
|
39
|
+
std::vector<System> _systems{};
|
|
40
|
+
std::atomic<bool> _isRunning{true};
|
|
41
|
+
std::atomic<bool> _isPaused{true};
|
|
42
|
+
double _tickRate{60.0};
|
|
43
|
+
World _world;
|
|
44
|
+
GameStats _gameStats;
|
|
45
|
+
std::unique_ptr<std::thread> _gameThread;
|
|
46
|
+
|
|
47
|
+
void runGameLoop();
|
|
48
|
+
void runSystems();
|
|
49
|
+
void update(double deltaTime);
|
|
50
|
+
void updateStats(double deltaTime);
|
|
51
|
+
void updateEntities(double deltaTime);
|
|
52
|
+
};
|
|
53
|
+
} // namespace margelo::nitro::rngine
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#include "GameMethods.hpp"
|
|
2
|
+
#include "GameLoop.hpp"
|
|
3
|
+
#include <android/log.h>
|
|
4
|
+
|
|
5
|
+
namespace margelo::nitro::rngine {
|
|
6
|
+
void GameMethods::setTickRate(double tickRate) {
|
|
7
|
+
auto &instance = GameLoop::getInstance();
|
|
8
|
+
std::lock_guard<std::mutex> lock(instance.getMutexInternal());
|
|
9
|
+
instance.setTickRate(tickRate);
|
|
10
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods",
|
|
11
|
+
"setTickRate: tickRate=%.1f", tickRate);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
void GameMethods::setWorld(const World &world) {
|
|
15
|
+
auto &instance = GameLoop::getInstance();
|
|
16
|
+
std::lock_guard<std::mutex> lock(instance.getMutexInternal());
|
|
17
|
+
instance.getWorldInternal() = world;
|
|
18
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods",
|
|
19
|
+
"setWorld: width=%.0f height=%.0f", world.width,
|
|
20
|
+
world.height);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
void GameMethods::setEntities(const std::vector<Entity> &entities) {
|
|
24
|
+
auto &instance = GameLoop::getInstance();
|
|
25
|
+
std::lock_guard<std::mutex> lock(instance.getMutexInternal());
|
|
26
|
+
auto &entitiesInternal = instance.getEntitiesInternal();
|
|
27
|
+
for (auto &entity : entities) {
|
|
28
|
+
entitiesInternal[entity.id] = entity;
|
|
29
|
+
}
|
|
30
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods",
|
|
31
|
+
"setEntities: loaded %zu entities", entities.size());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
void GameMethods::setSystems(const std::vector<System> &systems) {
|
|
35
|
+
auto &instance = GameLoop::getInstance();
|
|
36
|
+
std::lock_guard<std::mutex> lock(instance.getMutexInternal());
|
|
37
|
+
instance.getSystemsInternal() = systems;
|
|
38
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods",
|
|
39
|
+
"setSystems: loaded %zu systems", systems.size());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
void GameMethods::pause() {
|
|
43
|
+
auto &isPaused = GameLoop::getInstance().getIsPausedInternal();
|
|
44
|
+
if (isPaused) {
|
|
45
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods",
|
|
46
|
+
"Already paused, ignoring");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods", "Pause");
|
|
51
|
+
isPaused.store(true);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void GameMethods::resume() {
|
|
55
|
+
auto &isPaused = GameLoop::getInstance().getIsPausedInternal();
|
|
56
|
+
if (!isPaused) {
|
|
57
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods",
|
|
58
|
+
"Already running, ignoring");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods", "Resume");
|
|
62
|
+
isPaused.store(false);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
void GameMethods::spawn(const std::vector<Entity> &entities) {
|
|
66
|
+
auto &instance = GameLoop::getInstance();
|
|
67
|
+
std::lock_guard<std::mutex> lock(instance.getMutexInternal());
|
|
68
|
+
auto &entitiesInternal = instance.getEntitiesInternal();
|
|
69
|
+
|
|
70
|
+
for (const auto &entity : entities) {
|
|
71
|
+
if (entitiesInternal.count(entity.id)) {
|
|
72
|
+
__android_log_print(ANDROID_LOG_WARN, "GameMethods",
|
|
73
|
+
"spawn: entity already exists for id=%s",
|
|
74
|
+
entity.id.c_str());
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
entitiesInternal[entity.id] = entity;
|
|
78
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods",
|
|
79
|
+
"spawn: spawned entity id=%s", entity.id.c_str());
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void GameMethods::despawn(const std::string &id) {
|
|
84
|
+
auto &instance = GameLoop::getInstance();
|
|
85
|
+
std::lock_guard<std::mutex> lock(instance.getMutexInternal());
|
|
86
|
+
auto &entities = instance.getEntitiesInternal();
|
|
87
|
+
|
|
88
|
+
auto resolved = instance.resolveEntitiesInternal(id);
|
|
89
|
+
if (resolved.empty()) {
|
|
90
|
+
__android_log_print(ANDROID_LOG_WARN, "GameMethods",
|
|
91
|
+
"despawn: no entities found for id=%s", id.c_str());
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (auto *entity : resolved) {
|
|
96
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods",
|
|
97
|
+
"despawn: despawned entity id=%s", entity->id.c_str());
|
|
98
|
+
entities.erase(entity->id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
void GameMethods::setP(const std::string &id, double px, double py) {
|
|
103
|
+
auto &instance = GameLoop::getInstance();
|
|
104
|
+
std::lock_guard<std::mutex> lock(instance.getMutexInternal());
|
|
105
|
+
auto &entitiesInternal = instance.getEntitiesInternal();
|
|
106
|
+
auto it = entitiesInternal.find(id);
|
|
107
|
+
|
|
108
|
+
if (it == entitiesInternal.end()) {
|
|
109
|
+
__android_log_print(ANDROID_LOG_WARN, "GameMethods",
|
|
110
|
+
"setP: entity not found for id=%s", id.c_str());
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods",
|
|
114
|
+
"setP: entity found, setting px=%f py=%f", px, py);
|
|
115
|
+
it->second.px = px;
|
|
116
|
+
it->second.py = py;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
void GameMethods::setV(const std::string &id, double vx, double vy) {
|
|
120
|
+
auto &instance = GameLoop::getInstance();
|
|
121
|
+
std::lock_guard<std::mutex> lock(instance.getMutexInternal());
|
|
122
|
+
auto resolvedEntitiesInternals = instance.resolveEntitiesInternal(id);
|
|
123
|
+
|
|
124
|
+
if (resolvedEntitiesInternals.empty()) {
|
|
125
|
+
__android_log_print(ANDROID_LOG_WARN, "GameMethods",
|
|
126
|
+
"setV: entity not found for id=%s", id.c_str());
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
for (auto *entity : resolvedEntitiesInternals) {
|
|
130
|
+
entity->vx = vx;
|
|
131
|
+
entity->vy = vy;
|
|
132
|
+
}
|
|
133
|
+
__android_log_print(ANDROID_LOG_INFO, "GameMethods",
|
|
134
|
+
"setV: set vx=%f vy=%f for %zu entities matching id=%s",
|
|
135
|
+
vx, vy, resolvedEntitiesInternals.size(), id.c_str());
|
|
136
|
+
}
|
|
137
|
+
} // namespace margelo::nitro::rngine
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "HybridGameMethodsSpec.hpp"
|
|
4
|
+
|
|
5
|
+
namespace margelo::nitro::rngine {
|
|
6
|
+
class GameMethods : public HybridGameMethodsSpec {
|
|
7
|
+
public:
|
|
8
|
+
GameMethods() : HybridObject(TAG) {}
|
|
9
|
+
void setTickRate(double tickRate) override;
|
|
10
|
+
void setWorld(const World &world) override;
|
|
11
|
+
void setEntities(const std::vector<Entity> &entities) override;
|
|
12
|
+
void setSystems(const std::vector<System> &systems) override;
|
|
13
|
+
void pause() override;
|
|
14
|
+
void resume() override;
|
|
15
|
+
void spawn(const std::vector<Entity> &entities) override;
|
|
16
|
+
void despawn(const std::string &id) override;
|
|
17
|
+
void setP(const std::string &id, double px, double py) override;
|
|
18
|
+
void setV(const std::string &id, double vx, double vy) override;
|
|
19
|
+
};
|
|
20
|
+
} // namespace margelo::nitro::rngine
|
package/shared/Rect.hpp
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
namespace margelo::nitro::rngine {
|
|
4
|
+
static constexpr int RECT_SIZE = sizeof(double) * 4 + sizeof(uint32_t);
|
|
5
|
+
|
|
6
|
+
struct Rect {
|
|
7
|
+
double left;
|
|
8
|
+
double right;
|
|
9
|
+
double top;
|
|
10
|
+
double bottom;
|
|
11
|
+
uint32_t color;
|
|
12
|
+
};
|
|
13
|
+
} // namespace margelo::nitro::rngine
|