rngine 0.5.0 → 0.7.0

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/README.md CHANGED
@@ -21,28 +21,33 @@ configure({
21
21
  entities: [
22
22
  {
23
23
  id: 'player',
24
- px: 400,
25
- py: 400,
26
- width: 40,
27
- height: 40,
28
- color: '#00ff00',
24
+ px: 420,
25
+ py: 420,
26
+ color: '#0f0',
27
+ shape: {
28
+ radius: 20,
29
+ },
29
30
  },
30
31
  {
31
32
  id: 'enemy_1',
32
- px: 100,
33
- py: 100,
34
- width: 40,
35
- height: 40,
36
- color: '#ff0000',
33
+ px: 120,
34
+ py: 120,
35
+ color: '#f00',
36
+ shape: {
37
+ width: 40,
38
+ height: 40,
39
+ },
37
40
  vx: 1000,
38
41
  },
39
42
  {
40
43
  id: 'enemy_2',
41
- px: 200,
42
- py: 200,
43
- width: 40,
44
- height: 40,
45
- color: '#ff0000',
44
+ px: 220,
45
+ py: 220,
46
+ color: '#f00',
47
+ shape: {
48
+ width: 40,
49
+ height: 40,
50
+ },
46
51
  vx: -1000,
47
52
  },
48
53
  ],
@@ -53,7 +58,10 @@ configure({
53
58
  onTick: (enemies) => {
54
59
  enemies.forEach((enemy) => {
55
60
  // reverse direction when reaching screen edges
56
- if (enemy.px <= 0 || enemy.px + enemy.width >= 800) {
61
+ if (
62
+ enemy.px - enemy.width / 2 <= 0 ||
63
+ enemy.px + enemy.width / 2 >= 800
64
+ ) {
57
65
  update({ id: enemy.id, vx: -enemy.vx });
58
66
  }
59
67
  });
@@ -70,7 +78,7 @@ export default function App() {
70
78
 
71
79
  ## Concepts
72
80
 
73
- **Entities** are the objects in your game world. Each entity has a position, size, color, and velocity. See [`Entity`](./src/nativeTypes.ts).
81
+ **Entities** are the objects in your game world. Each entity has a position, shape, color, and velocity. See [`Entity`](./src/nativeTypes.ts).
74
82
 
75
83
  **Systems** define your game logic. Each system optionally declares which entities it cares about via `entities`, which collision pairs to watch via `collisions`, or both. Systems run every tick receiving the resolved entities and any active collisions. See [`System`](./src/types.ts).
76
84
 
@@ -13,6 +13,7 @@ add_library(
13
13
  src/main/cpp/GameLoopJNI.cpp
14
14
  ../shared/GameLoop.cpp
15
15
  ../shared/GameMethods.cpp
16
+ ../shared/CollisionUtils.cpp
16
17
  )
17
18
 
18
19
  # Add Nitrogen specs :)
Binary file
@@ -1,36 +1,126 @@
1
+ #include "ColorUtils.hpp"
1
2
  #include "GameLoop.hpp"
3
+ #include <cstdint>
2
4
  #include <jni.h>
5
+ #include <mutex>
6
+ #include <variant>
3
7
 
4
8
  extern "C" {
5
- JNIEXPORT jobject JNICALL
6
- Java_com_margelo_nitro_rngine_GameView_getRectsSnapshot(JNIEnv *env, jobject) {
7
- auto rects =
8
- margelo::nitro::rngine::GameLoop::getInstance().getRectsSnapshot();
9
- const size_t size = rects.size() * margelo::nitro::rngine::RECT_SIZE;
10
-
11
- static std::vector<uint8_t> buffer;
12
- buffer.resize(size);
13
-
14
- uint8_t *data = buffer.data();
15
-
16
- for (const auto &rect : rects) {
17
- memcpy(data, &rect.left, sizeof(float));
18
- data += sizeof(float);
19
- memcpy(data, &rect.right, sizeof(float));
20
- data += sizeof(float);
21
- memcpy(data, &rect.top, sizeof(float));
22
- data += sizeof(float);
23
- memcpy(data, &rect.bottom, sizeof(float));
24
- data += sizeof(float);
25
- memcpy(data, &rect.progress, sizeof(float));
26
- data += sizeof(float);
27
- memcpy(data, &rect.color, sizeof(uint32_t));
28
- data += sizeof(uint32_t);
29
- memcpy(data, &rect.asset, sizeof(int32_t));
30
- data += sizeof(int32_t);
9
+ JNIEXPORT jbyteArray JNICALL
10
+ Java_com_margelo_nitro_rngine_GameView_getSnapshot(JNIEnv *env, jobject) {
11
+ auto &gameLoop = margelo::nitro::rngine::GameLoop::getInstance();
12
+ auto &snapshotMutex = gameLoop.getSnapshotMutexInternal();
13
+
14
+ std::lock_guard<std::mutex> snapshotLock(snapshotMutex);
15
+ auto &screenSnapshot = gameLoop.getScreenSnapshotInternal();
16
+ auto &entitiesSnapshot = gameLoop.getEntitiesSnapshotInternal();
17
+
18
+ std::vector<uint8_t> buffer;
19
+
20
+ auto writeFloat = [&](float v) {
21
+ const auto *bytes = reinterpret_cast<const uint8_t *>(&v);
22
+ buffer.insert(buffer.end(), bytes, bytes + sizeof(float));
23
+ };
24
+ auto writeI32 = [&](int32_t v) {
25
+ const auto *bytes = reinterpret_cast<const uint8_t *>(&v);
26
+ buffer.insert(buffer.end(), bytes, bytes + sizeof(int32_t));
27
+ };
28
+ auto writeU32 = [&](uint32_t v) {
29
+ const auto *bytes = reinterpret_cast<const uint8_t *>(&v);
30
+ buffer.insert(buffer.end(), bytes, bytes + sizeof(uint32_t));
31
+ };
32
+
33
+ const auto screenSnapshotWidth = static_cast<float>(screenSnapshot.width);
34
+ const auto screenSnapshotHeight = static_cast<float>(screenSnapshot.height);
35
+ const auto screenSnapshotColor =
36
+ margelo::nitro::rngine::parseHexColor(screenSnapshot.color);
37
+ const auto screenSnapshotAsset =
38
+ static_cast<int32_t>(screenSnapshot.asset.value_or(0));
39
+
40
+ writeFloat(screenSnapshotWidth);
41
+ writeFloat(screenSnapshotHeight);
42
+ writeU32(screenSnapshotColor);
43
+ writeI32(screenSnapshotAsset);
44
+
45
+ if (screenSnapshotAsset < 0) {
46
+ writeFloat(static_cast<float>(screenSnapshot.progress.value_or(0)));
31
47
  }
32
48
 
33
- return env->NewDirectByteBuffer(buffer.data(), (jlong)size);
49
+ for (const auto &[_, entitySnapshot] : entitiesSnapshot) {
50
+ uint8_t shapeType{0};
51
+ float entitySnapshotLeft{0}, entitySnapshotRight{0}, entitySnapshotTop{0},
52
+ entitySnapshotBottom{0}, circleRadius{0};
53
+
54
+ std::visit(
55
+ [&](const auto &shape) {
56
+ using T = std::decay_t<decltype(shape)>;
57
+ if constexpr (std::is_same_v<T, margelo::nitro::rngine::Rect>) {
58
+ shapeType = 0;
59
+ entitySnapshotLeft =
60
+ static_cast<float>(entitySnapshot.px - shape.width / 2.0);
61
+ entitySnapshotRight =
62
+ static_cast<float>(entitySnapshot.px + shape.width / 2.0);
63
+ entitySnapshotTop =
64
+ static_cast<float>(entitySnapshot.py - shape.height / 2.0);
65
+ entitySnapshotBottom =
66
+ static_cast<float>(entitySnapshot.py + shape.height / 2.0);
67
+ } else {
68
+ shapeType = 1;
69
+ circleRadius = static_cast<float>(shape.radius);
70
+ entitySnapshotLeft =
71
+ static_cast<float>(entitySnapshot.px - shape.radius);
72
+ entitySnapshotRight =
73
+ static_cast<float>(entitySnapshot.px + shape.radius);
74
+ entitySnapshotTop =
75
+ static_cast<float>(entitySnapshot.py - shape.radius);
76
+ entitySnapshotBottom =
77
+ static_cast<float>(entitySnapshot.py + shape.radius);
78
+ }
79
+ },
80
+ entitySnapshot.shape);
81
+
82
+ if (entitySnapshotRight < 0 || entitySnapshotLeft > screenSnapshot.width ||
83
+ entitySnapshotBottom < 0 || entitySnapshotTop > screenSnapshot.height) {
84
+ continue;
85
+ }
86
+
87
+ const auto entitySnapshotColor =
88
+ margelo::nitro::rngine::parseHexColor(entitySnapshot.color);
89
+ const auto entitySnapshotAsset =
90
+ static_cast<int32_t>(entitySnapshot.asset.value_or(0));
91
+
92
+ buffer.push_back(shapeType);
93
+
94
+ if (shapeType == 0) {
95
+ writeFloat(entitySnapshotLeft);
96
+ writeFloat(entitySnapshotRight);
97
+ writeFloat(entitySnapshotTop);
98
+ writeFloat(entitySnapshotBottom);
99
+ } else {
100
+ writeFloat(static_cast<float>(entitySnapshot.px));
101
+ writeFloat(static_cast<float>(entitySnapshot.py));
102
+ writeFloat(circleRadius);
103
+ }
104
+
105
+ writeU32(entitySnapshotColor);
106
+ writeI32(entitySnapshotAsset);
107
+
108
+ if (entitySnapshotAsset < 0) {
109
+ writeFloat(static_cast<float>(entitySnapshot.progress.value_or(0)));
110
+ }
111
+ }
112
+
113
+ auto bufferSize = static_cast<jsize>(buffer.size());
114
+ jbyteArray result = env->NewByteArray(bufferSize);
115
+
116
+ if (result == nullptr) {
117
+ return nullptr;
118
+ }
119
+
120
+ env->SetByteArrayRegion(result, 0, bufferSize,
121
+ reinterpret_cast<const jbyte *>(buffer.data()));
122
+
123
+ return result;
34
124
  }
35
125
 
36
126
  JNIEXPORT void JNICALL
@@ -6,6 +6,7 @@ import com.airbnb.lottie.LottieDrawable
6
6
  import com.caverock.androidsvg.SVG
7
7
  import com.margelo.nitro.core.Promise
8
8
  import java.net.URL
9
+ import com.margelo.nitro.NitroModules
9
10
 
10
11
  class GameAssets : HybridGameAssetsSpec() {
11
12
  private external fun registerLottieDuration(id: Double, duration: Double)
@@ -22,7 +23,16 @@ class GameAssets : HybridGameAssetsSpec() {
22
23
  override fun registerSvg(id: Double, uri: String): Promise<Unit> {
23
24
  return Promise.async {
24
25
  try {
25
- val stream = URL(uri).openStream()
26
+ val stream = if (uri.startsWith("http://") || uri.startsWith("https://")) {
27
+ URL(uri).openStream()
28
+ } else {
29
+ val context = NitroModules.applicationContext ?: throw Error("No Context available!")
30
+ val resId = context.resources.getIdentifier(uri, "raw", context.packageName)
31
+ if (resId == 0) {
32
+ throw IllegalStateException("Could not resolve drawable resource for asset: $uri")
33
+ }
34
+ context.resources.openRawResource(resId)
35
+ }
26
36
  assetCache[id.toInt()] = Asset.Svg(SVG.getFromInputStream(stream).renderToPicture())
27
37
  Log.d("GameAssets", "assetCache: $assetCache")
28
38
  } catch (e: Exception) {
@@ -1,13 +1,11 @@
1
1
  package com.margelo.nitro.rngine
2
2
 
3
3
  import android.content.Context
4
- import android.graphics.Color
4
+ import android.graphics.Canvas
5
5
  import android.graphics.Paint
6
6
  import android.util.AttributeSet
7
- import android.util.Log
8
7
  import android.view.SurfaceView
9
8
  import android.view.View
10
- import java.nio.ByteBuffer
11
9
  import androidx.core.graphics.withTranslation
12
10
  import androidx.core.graphics.withClip
13
11
 
@@ -20,15 +18,98 @@ class GameView(
20
18
  attrs,
21
19
  defStyleAttr,
22
20
  ) {
23
- private external fun getRectsSnapshot(): ByteBuffer
21
+ private external fun getSnapshot(): ByteArray
24
22
  var onAttached: () -> Unit = {}
25
23
  var onDetached: () -> Unit = {}
26
24
 
27
25
  private val paint = Paint().apply {
28
- color = Color.TRANSPARENT
29
26
  style = Paint.Style.FILL
30
27
  }
31
28
 
29
+ private fun drawAsset(
30
+ canvas: Canvas,
31
+ left: Float,
32
+ top: Float,
33
+ right: Float,
34
+ bottom: Float,
35
+ asset: Int,
36
+ progress: Float?,
37
+ ) {
38
+ canvas.withTranslation(left, top) {
39
+ when (val resolvedAsset = GameAssets.getAsset(asset)) {
40
+ is Asset.Svg -> {
41
+ scale(
42
+ (right - left) / resolvedAsset.picture.width,
43
+ (bottom - top) / resolvedAsset.picture.height
44
+ )
45
+ drawPicture(resolvedAsset.picture)
46
+ }
47
+
48
+ is Asset.Lottie -> {
49
+ progress?.let {
50
+ resolvedAsset.drawable.progress = it
51
+ }
52
+ resolvedAsset.drawable.setBounds(0, 0, (right - left).toInt(), (bottom - top).toInt())
53
+ resolvedAsset.drawable.draw(canvas)
54
+ }
55
+
56
+ null -> {}
57
+ }
58
+ }
59
+ }
60
+
61
+ private fun drawRect(
62
+ canvas: Canvas,
63
+ left: Float,
64
+ top: Float,
65
+ right: Float,
66
+ bottom: Float,
67
+ color: Int,
68
+ asset: Int,
69
+ progress: Float?,
70
+ ) {
71
+ paint.color = color
72
+ canvas.drawRect(left, top, right, bottom, paint)
73
+
74
+ drawAsset(
75
+ canvas,
76
+ left,
77
+ top,
78
+ right,
79
+ bottom,
80
+ asset,
81
+ progress
82
+ )
83
+ }
84
+
85
+ private fun drawCircle(
86
+ canvas: Canvas,
87
+ cx: Float,
88
+ cy: Float,
89
+ radius: Float,
90
+ color: Int,
91
+ asset: Int,
92
+ progress: Float?,
93
+ ) {
94
+ val left = cx - radius
95
+ val top = cy - radius
96
+ val right = cx + radius
97
+ val bottom = cy + radius
98
+
99
+ paint.color = color
100
+ canvas.drawCircle(cx, cy, radius, paint)
101
+
102
+ drawAsset(
103
+ canvas,
104
+ left,
105
+ top,
106
+ right,
107
+ bottom,
108
+ asset,
109
+ progress,
110
+ )
111
+ }
112
+
32
113
  init {
33
114
  addOnAttachStateChangeListener(
34
115
  object : OnAttachStateChangeListener {
@@ -40,60 +121,65 @@ class GameView(
40
121
  fun drawFrame() {
41
122
  if (!holder.surface.isValid) return
42
123
  val canvas = holder.lockCanvas() ?: return
124
+ val snapshot = SnapshotSerializer.decode(getSnapshot())
43
125
 
44
- val rects = RectSerializer.decode(getRectsSnapshot())
45
-
46
- val worldRect = rects.first()
47
- val scaleX = canvas.width / worldRect.right
48
- val scaleY = canvas.height / worldRect.bottom
126
+ val scaleX = canvas.width / snapshot.screen.width
127
+ val scaleY = canvas.height / snapshot.screen.height
49
128
  val scale = minOf(scaleX, scaleY)
50
129
 
51
- val offsetX = (canvas.width - worldRect.right * scale) / 2f
52
- val offsetY = (canvas.height - worldRect.bottom * scale) / 2f
53
-
54
- rects.forEach { rect ->
55
- val left = rect.left * scale + offsetX
56
- val top = rect.top * scale + offsetY
57
- val right = rect.right * scale + offsetX
58
- val bottom = rect.bottom * scale + offsetY
59
-
60
- val clampedLeft = left.coerceAtLeast(offsetX)
61
- val clampedTop = top.coerceAtLeast(offsetY)
62
- val clampedRight = right.coerceAtMost(offsetX + worldRect.right * scale)
63
- val clampedBottom = bottom.coerceAtMost(offsetY + worldRect.bottom * scale)
64
-
65
- paint.color = rect.color
66
- canvas.drawRect(
67
- clampedLeft,
68
- clampedTop,
69
- clampedRight,
70
- clampedBottom,
71
- paint
72
- )
73
- when(val asset = GameAssets.getAsset(rect.asset)){
74
- is Asset.Svg -> {
75
- canvas.withClip(clampedLeft, clampedTop, clampedRight, clampedBottom) {
76
- withTranslation(left, top) {
77
- scale(
78
- (right - left) / asset.picture.width,
79
- (bottom - top) / asset.picture.height
80
- )
81
- drawPicture(asset.picture)
82
- }
130
+ val screenLeft = (canvas.width - snapshot.screen.width * scale) / 2f
131
+ val screenTop = (canvas.height - snapshot.screen.height * scale) / 2f
132
+ val screenRight = screenLeft + snapshot.screen.width * scale
133
+ val screenBottom = screenTop + snapshot.screen.height * scale
134
+
135
+ drawRect(
136
+ canvas,
137
+ screenLeft,
138
+ screenTop,
139
+ screenRight,
140
+ screenBottom,
141
+ snapshot.screen.color,
142
+ snapshot.screen.asset,
143
+ snapshot.screen.progress,
144
+ )
145
+
146
+ canvas.withClip(screenLeft, screenTop, screenRight, screenBottom) {
147
+ snapshot.shapes.forEach { shape ->
148
+ when (shape) {
149
+ is Shape.Rect -> {
150
+ val left = shape.left * scale + screenLeft
151
+ val top = shape.top * scale + screenTop
152
+ val right = shape.right * scale + screenLeft
153
+ val bottom = shape.bottom * scale + screenTop
154
+
155
+ drawRect(
156
+ canvas,
157
+ left,
158
+ top,
159
+ right,
160
+ bottom,
161
+ shape.color,
162
+ shape.asset,
163
+ shape.progress,
164
+ )
83
165
  }
84
- }
85
166
 
86
- is Asset.Lottie -> {
87
- canvas.withClip(clampedLeft, clampedTop, clampedRight, clampedBottom) {
88
- withTranslation(left, top) {
89
- asset.drawable.progress = rect.progress
90
- asset.drawable.setBounds(0, 0, (right - left).toInt(), (bottom - top).toInt())
91
- asset.drawable.draw(canvas)
92
- }
167
+ is Shape.Circle -> {
168
+ val cx = shape.px * scale + screenLeft
169
+ val cy = shape.py * scale + screenTop
170
+ val radius = shape.radius * scale
171
+
172
+ drawCircle(
173
+ canvas,
174
+ cx,
175
+ cy,
176
+ radius,
177
+ shape.color,
178
+ shape.asset,
179
+ shape.progress,
180
+ )
93
181
  }
94
182
  }
95
-
96
- null -> {}
97
183
  }
98
184
  }
99
185
  holder.unlockCanvasAndPost(canvas)
@@ -0,0 +1,9 @@
1
+ package com.margelo.nitro.rngine
2
+
3
+ data class Screen(
4
+ val width: Float,
5
+ val height: Float,
6
+ val color: Int,
7
+ val asset: Int,
8
+ val progress: Float?,
9
+ )
@@ -0,0 +1,26 @@
1
+ package com.margelo.nitro.rngine
2
+
3
+ sealed class Shape(
4
+ val color: Int,
5
+ val asset: Int,
6
+ val progress: Float?,
7
+ ) {
8
+ class Rect(
9
+ val left: Float,
10
+ val right: Float,
11
+ val top: Float,
12
+ val bottom: Float,
13
+ color: Int,
14
+ asset: Int,
15
+ progress: Float?,
16
+ ) : Shape(color, asset, progress)
17
+
18
+ class Circle(
19
+ val px: Float,
20
+ val py: Float,
21
+ val radius: Float,
22
+ color: Int,
23
+ asset: Int,
24
+ progress: Float?,
25
+ ) : Shape(color, asset, progress)
26
+ }
@@ -0,0 +1,3 @@
1
+ package com.margelo.nitro.rngine
2
+
3
+ data class Snapshot(val screen: Screen, val shapes: List<Shape>)
@@ -0,0 +1,53 @@
1
+ package com.margelo.nitro.rngine
2
+
3
+ import android.util.Log
4
+ import java.nio.ByteBuffer
5
+ import java.nio.ByteOrder
6
+
7
+ object SnapshotSerializer {
8
+ fun decode(bytes: ByteArray): Snapshot {
9
+ val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.nativeOrder())
10
+
11
+ val width = buffer.float
12
+ val height = buffer.float
13
+ val color = buffer.int
14
+ val asset = buffer.int
15
+ val progress = if (asset < 0) buffer.float else null
16
+
17
+ val screen = Screen(width, height, color, asset, progress)
18
+ val shapes = ArrayList<Shape>()
19
+
20
+ while (buffer.hasRemaining()) {
21
+ when (val shapeType = buffer.get().toInt()) {
22
+ 0 -> {
23
+ val left = buffer.float
24
+ val right = buffer.float
25
+ val top = buffer.float
26
+ val bottom = buffer.float
27
+
28
+ val color = buffer.int
29
+ val asset = buffer.int
30
+ val progress = if (asset < 0) buffer.float else null
31
+
32
+ shapes.add(Shape.Rect(left, right, top, bottom, color, asset, progress))
33
+ }
34
+
35
+ 1 -> {
36
+ val px = buffer.float
37
+ val py = buffer.float
38
+ val radius = buffer.float
39
+
40
+ val color = buffer.int
41
+ val asset = buffer.int
42
+ val progress = if (asset < 0) buffer.float else null
43
+
44
+ shapes.add(Shape.Circle(px, py, radius, color, asset, progress))
45
+ }
46
+
47
+ else -> Log.e("SnapshotSerializer", "Unknown shapeType: $shapeType")
48
+ }
49
+ }
50
+
51
+ return Snapshot(screen, shapes)
52
+ }
53
+ }