diff --git a/.expo-shared/assets.json b/.expo-shared/assets.json
new file mode 100644
index 0000000..1e6decf
--- /dev/null
+++ b/.expo-shared/assets.json
@@ -0,0 +1,4 @@
+{
+ "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
+ "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d0637c2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+node_modules/
+.expo/
+dist/
+npm-debug.*
+*.jks
+*.p8
+*.p12
+*.key
+*.mobileprovision
+*.orig.*
+web-build/
+
+# macOS
+.DS_Store
+
+yarn.lock
+package-lock.json
diff --git a/App.tsx b/App.tsx
new file mode 100644
index 0000000..c1c37d7
--- /dev/null
+++ b/App.tsx
@@ -0,0 +1,15 @@
+import { StatusBar } from "expo-status-bar";
+import { SafeAreaProvider } from "react-native-safe-area-context";
+
+import AppView from "./AppView";
+
+export default function App() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/AppView.tsx b/AppView.tsx
new file mode 100644
index 0000000..bb1850c
--- /dev/null
+++ b/AppView.tsx
@@ -0,0 +1,74 @@
+import React, { useEffect, useState } from "react";
+import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
+import { GameEngine } from "react-native-game-engine";
+import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
+
+import { entities, useEntities } from "@entities";
+import { GameLoop } from "@systems";
+import { IGameEngine } from "@types";
+
+export default function AppView() {
+ const [isRunning, setIsRunning] = React.useState(true);
+ const [score, setScore] = useState(0);
+ const [gameEngine, setGameEngine] = useState(null);
+ const { entities } = useEntities();
+ const { top } = useSafeAreaInsets();
+ const topInset = top.valueOf();
+ (global as any).topInset = topInset;
+
+ const gameEntities = entities();
+ const { engine, world } = gameEntities.physics;
+
+ // console.log(topInset)
+
+ // const gameEntities = entities();
+ useEffect(() => {
+ (global as any).gameEngine = gameEngine;
+ }, [gameEngine]);
+
+ return (
+
+ {/* @ts-ignore */}
+ setGameEngine(ref as IGameEngine)}
+ systems={[GameLoop]}
+ entities={gameEntities}
+ running={isRunning}
+ onEvent={({ type, ...rest }: any) => {
+ switch (type) {
+ case "addToScore": {
+ setScore(score => score + 1);
+ break;
+ }
+ }
+ }}
+ style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }}
+ >
+ {
+ gameEngine?.swap(entities());
+ }}
+ >
+
+ {score}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+});
diff --git a/app.json b/app.json
new file mode 100644
index 0000000..bba08a9
--- /dev/null
+++ b/app.json
@@ -0,0 +1,33 @@
+{
+ "expo": {
+ "name": "react-native-game-engine-expo-typescript-template",
+ "slug": "react-native-game-engine-expo-typescript-template",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "userInterfaceStyle": "light",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/assets/SVG/BalloonSVG.js b/assets/SVG/BalloonSVG.js
new file mode 100644
index 0000000..2a00279
--- /dev/null
+++ b/assets/SVG/BalloonSVG.js
@@ -0,0 +1,25 @@
+import * as React from "react";
+import Svg, { G, Path } from "react-native-svg";
+
+export const BalloonSVG = (props) => {
+ const color = props.color || "#000";
+ return (
+
+ )
+};
+
diff --git a/assets/SVG/index.ts b/assets/SVG/index.ts
new file mode 100644
index 0000000..f7c52bd
--- /dev/null
+++ b/assets/SVG/index.ts
@@ -0,0 +1 @@
+export * from './BalloonSVG';
\ No newline at end of file
diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png
new file mode 100644
index 0000000..03d6f6b
Binary files /dev/null and b/assets/adaptive-icon.png differ
diff --git a/assets/favicon.png b/assets/favicon.png
new file mode 100644
index 0000000..e75f697
Binary files /dev/null and b/assets/favicon.png differ
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 0000000..a0b1526
Binary files /dev/null and b/assets/icon.png differ
diff --git a/assets/splash.png b/assets/splash.png
new file mode 100644
index 0000000..0e89705
Binary files /dev/null and b/assets/splash.png differ
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000..78f01ea
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,22 @@
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ plugins: [
+ [
+ 'module-resolver',
+ {
+ root: ['./src'],
+ extensions: ['.ios.ts', '.android.ts', '.ts', '.ios.tsx', '.android.tsx', '.jsx', '.js', '.json'],
+ alias: {
+ '@entities': './game/entities',
+ "@game": "./game",
+ '@systems': './game/systems',
+ '@svg': './assets/SVG',
+ '@types': './game',
+ },
+ },
+ ],
+ ],
+ };
+};
diff --git a/game/entities/Balloon.tsx b/game/entities/Balloon.tsx
new file mode 100644
index 0000000..a4be021
--- /dev/null
+++ b/game/entities/Balloon.tsx
@@ -0,0 +1,60 @@
+import Matter from 'matter-js'
+import React from 'react'
+import { ColorValue, View } from 'react-native'
+
+import { BalloonSVG } from '@svg'
+import { Position2D, Size2D } from '@types'
+
+const Balloon = ({ body, color }: any) => {
+ const widthBody = body.bounds.max.x - body.bounds.min.x;
+ const heightBody = body.bounds.max.y - body.bounds.min.y;
+
+ const xBody = body.position.x - widthBody / 2;
+ const yBody = body.position.y - heightBody / 2;
+
+ return (
+
+
+
+ );
+ };
+
+ export default (
+ world: Matter.Composite,
+ color: ColorValue,
+ pos: Position2D,
+ size: Size2D
+ ) => {
+ const body = Matter.Bodies.circle(pos.x, pos.y, 50, {
+ label: "Balloon",
+ isStatic: false,
+ // restitution: 0.4,
+ // friction: 1,
+ frictionAir: 0.2,
+ // mass: 0.1,
+ // inverseMass: 0.1,
+ // bounds: {
+ // min: { x: size.width, y: size.height },
+ // max: { x: size.width, y: size.height },
+ // },
+ // inertia: Infinity,
+ // inverseInertia: Infinity,
+ } as Matter.IChamferableBodyDefinition);
+ Matter.Composite.add(world, body);
+
+ return {
+ body,
+ color,
+ pos,
+ renderer: Balloon,
+ };
+ };
+
\ No newline at end of file
diff --git a/game/entities/Finger.tsx b/game/entities/Finger.tsx
new file mode 100644
index 0000000..61ce0f1
--- /dev/null
+++ b/game/entities/Finger.tsx
@@ -0,0 +1,26 @@
+import React, { PureComponent } from "react";
+import { StyleSheet, View } from "react-native";
+
+const RADIUS = 20;
+
+export const Finger = ({ position }: { position: any[] }) => {
+ const x = position[0] - RADIUS / 2;
+ const y = position[1] - RADIUS / 2;
+ return (
+
+ )
+};
+
+const styles = StyleSheet.create({
+ finger: {
+ borderColor: "#CCC",
+ borderWidth: 4,
+ borderRadius: RADIUS * 2,
+ width: RADIUS * 2,
+ height: RADIUS * 2,
+ backgroundColor: "pink",
+ position: "absolute"
+ }
+});
+
+export default Finger;
\ No newline at end of file
diff --git a/game/entities/Wall.tsx b/game/entities/Wall.tsx
new file mode 100644
index 0000000..131094a
--- /dev/null
+++ b/game/entities/Wall.tsx
@@ -0,0 +1,45 @@
+import Matter from 'matter-js'
+import React from 'react'
+import { ColorValue, View } from 'react-native'
+
+import { Position2D, Size2D } from '@types'
+
+const Wall = ({ body, color }: any) => {
+ const widthBody = body.bounds.max.x - body.bounds.min.x
+ const heightBody = body.bounds.max.y - body.bounds.min.y
+
+ const xBody = body.position.x - widthBody / 2
+ const yBody = body.position.y - heightBody / 2
+
+ return (
+
+ )
+}
+
+export default (world: Matter.Composite, color: ColorValue, pos: Position2D, size: Size2D) => {
+ const body = Matter.Bodies.rectangle(
+ pos.x,
+ pos.y,
+ size.width,
+ size.height,
+ {
+ label: 'Wall',
+ isStatic: true
+ } as Matter.IChamferableBodyDefinition
+ )
+ Matter.Composite.add(world, body)
+
+ return {
+ body,
+ color,
+ pos,
+ renderer: Wall
+ }
+}
diff --git a/game/entities/entities.ts b/game/entities/entities.ts
new file mode 100644
index 0000000..7e26ab4
--- /dev/null
+++ b/game/entities/entities.ts
@@ -0,0 +1,101 @@
+import { Dimensions } from "react-native";
+import Matter from "matter-js";
+
+import { windowHeight, windowWidth } from "@game";
+import { Balloon, Finger, Wall } from ".";
+
+export const entities = (restart: boolean = false) => {
+ let engine = Matter.Engine.create(undefined, {
+ enableSleeping: false,
+ gravity: { x: 0, y: 0.0005 },
+ } as Matter.IEngineDefinition);
+
+ let world = engine.world;
+ const topInset = (global as any).topInset; // for notch handling
+
+ const newBalloon = () => {
+ return Balloon(
+ world,
+ "red",
+ {
+ x: Math.random() * (windowWidth - 100) + 50,
+ y: 150,
+ },
+ { width: 50, height: 50 }
+ );
+ };
+
+ let entities = {
+ physics: { engine, world },
+ fingers: {
+ 1: { position: [40, 200], renderer: Finger },
+ 2: { position: [100, 200], renderer: Finger },
+ 3: { position: [160, 200], renderer: Finger },
+ 4: { position: [220, 200], renderer: Finger },
+ 5: { position: [280, 200], renderer: Finger },
+ },
+ Balloon: newBalloon(),
+ LeftWall: Wall(
+ world,
+ "orange",
+ { x: 0 - 25, y: windowHeight / 2 },
+ { height: windowHeight, width: 50 }
+ ),
+ RightWall: Wall(
+ world,
+ "orange",
+ { x: windowWidth + 25, y: windowHeight / 2 },
+ { height: windowHeight, width: 50 }
+ ),
+ Ceiling: Wall(
+ world,
+ "orange",
+ { x: 0, y: 0 },
+ { height: 110 + topInset, width: windowWidth * 2 }
+ ),
+ Floor: Wall(
+ world,
+ "orange",
+ { x: windowWidth / 2, y: windowHeight + 60 },
+ { height: 60, width: windowWidth }
+ ),
+ };
+
+ Matter.Events.on(
+ engine,
+ "collisionStart",
+ ({ pairs }: Matter.IEventCollision) => {
+ for (var i = 0, j = pairs.length; i != j; ++i) {
+ const bodyA = pairs[i].bodyA;
+ const bodyB = pairs[i].bodyB;
+ console.log(
+ "collisionStart between " + bodyA.label + " - " + bodyB.label
+ );
+ const balloonBody = entities.Balloon.body;
+ Matter.Body.scale(balloonBody, 0.0, 0.0, {
+ x: balloonBody.bounds.min.x - 1000,
+ y: balloonBody.bounds.max.y,
+ });
+ Matter.World.remove(world, balloonBody, true);
+
+ const gameEngine = (global as any).gameEngine;
+ gameEngine.dispatch({
+ type: "addToScore",
+ });
+
+ entities.Balloon = newBalloon();
+
+ // @ts-ignore
+ Matter.World.add(world, entities.Balloon);
+ }
+ }
+ );
+
+ return entities;
+};
+
+export const useEntities = () => {
+ return {
+ entities
+ }
+}
\ No newline at end of file
diff --git a/game/entities/index.ts b/game/entities/index.ts
new file mode 100644
index 0000000..b147725
--- /dev/null
+++ b/game/entities/index.ts
@@ -0,0 +1,16 @@
+import { Dimensions } from "react-native";
+import Matter from "matter-js";
+
+export { windowHeight, windowWidth } from "@game";
+
+import Finger from "./Finger";
+import Wall from "./Wall";
+import Balloon from './Balloon';
+
+export {
+ Finger,
+ Wall,
+ Balloon
+};
+
+export * from './entities';
diff --git a/game/index.ts b/game/index.ts
new file mode 100644
index 0000000..33607f4
--- /dev/null
+++ b/game/index.ts
@@ -0,0 +1,4 @@
+import { Dimensions } from "react-native";
+
+export const windowHeight = Dimensions.get("window").height;
+export const windowWidth = Dimensions.get("window").width;
diff --git a/game/systems/GameLoop.ts b/game/systems/GameLoop.ts
new file mode 100644
index 0000000..491077c
--- /dev/null
+++ b/game/systems/GameLoop.ts
@@ -0,0 +1,23 @@
+import Matter, { Vector } from "matter-js";
+import { GameEngineUpdateEventOptionType, TouchEvent } from "react-native-game-engine";
+
+import { windowHeight, windowWidth } from "@game";
+
+export const GameLoop = (
+ entities: any,
+ { touches, time, dispatch }: GameEngineUpdateEventOptionType
+) => {
+ let engine = entities.physics.engine;
+ let world = entities.physics.world;
+
+ touches
+ .filter((t: TouchEvent) => t.type === "press")
+ .forEach((t: TouchEvent) => {
+ let balloonPos = entities.Balloon.body.position;
+ console.log('Touch:', t, balloonPos);
+ // Matter.Body.setVelocity(something, { x: something.velocity.x + 20, y: something.velocity.y - 20 });
+ });
+
+ Matter.Engine.update(engine, time.delta);
+ return entities;
+};
diff --git a/game/systems/index.ts b/game/systems/index.ts
new file mode 100644
index 0000000..fc43f5e
--- /dev/null
+++ b/game/systems/index.ts
@@ -0,0 +1,2 @@
+export * from './GameLoop';
+
diff --git a/game/types.ts b/game/types.ts
new file mode 100644
index 0000000..b787fcb
--- /dev/null
+++ b/game/types.ts
@@ -0,0 +1,18 @@
+import { GameEngine } from "react-native-game-engine";
+
+export interface Position2D {
+ x: number;
+ y: number;
+}
+
+export interface Size2D {
+ width: number;
+ height: number;
+}
+
+export interface IGameEngine extends GameEngine {
+ stop: () => void;
+ start: () => void;
+ swap: (newEntities: Promise | any) => void;
+ dispatch: (event: string) => void;
+ }
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6f96186
--- /dev/null
+++ b/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "react-native-game-engine-expo-typescript-template",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "expo": "~45.0.0",
+ "expo-status-bar": "~1.3.0",
+ "matter-js": "^0.18.0",
+ "react": "17.0.2",
+ "react-dom": "17.0.2",
+ "react-native": "0.68.2",
+ "react-native-game-engine": "^1.2.0",
+ "react-native-safe-area-context": "4.2.4",
+ "react-native-svg": "12.3.0",
+ "react-native-web": "0.17.7"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.17.12",
+ "@babel/plugin-proposal-class-properties": "^7.17.12",
+ "@babel/preset-env": "^7.17.12",
+ "@expo/webpack-config": "^0.16.24",
+ "@types/matter-js": "^0.17.7",
+ "@types/react": "~17.0.21",
+ "@types/react-native": "~0.66.13",
+ "typescript": "~4.3.5"
+ },
+ "private": true,
+ "peerDependencies": {
+ "rxjs": "6.6.7"
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..60ad4aa
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "expo/tsconfig.base",
+ "compilerOptions": {
+ "importHelpers": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "baseUrl": ".",
+ "module": "esnext",
+ "paths": {
+ "@entities": ["game/entities/index.ts"],
+ "@game": ["game/index.ts"],
+ "@svg": ["assets/SVG/index.ts"],
+ "@systems": ["game/systems/index.ts"],
+ "@types": ["game/types.ts"],
+ }
+ }
+}
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..4b0d8eb
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,46 @@
+const createExpoWebpackConfigAsync = require('@expo/webpack-config');
+
+// Expo CLI will await this method so you can optionally return a promise.
+module.exports = async function (env, argv) {
+ const config = await createExpoWebpackConfigAsync(env, argv);
+
+ config.performance = {
+ hints: false,
+ maxEntrypointSize: 512000,
+ maxAssetSize: 512000,
+ };
+
+ config.optimization = {
+ splitChunks: {
+ chunks: 'all',
+ maxSize: 512000,
+ maxAsyncRequests: 50,
+ maxInitialRequests: 30,
+ minChunks: 1,
+ cacheGroups: {
+ defaultVendors: {
+ test: /[\\/]node_modules[\\/]/,
+ priority: -10,
+ reuseExistingChunk: true,
+ },
+ default: {
+ minChunks: 2,
+ priority: -20,
+ reuseExistingChunk: true,
+ },
+ },
+ },
+ };
+
+ // Don't compress the development build
+ if (config.mode === 'development') {
+ config.devServer.compress = false;
+ }
+
+ if (config.mode === 'production') {
+ config.optimization.minimize = true;
+ }
+
+ // Finally return the new config for the CLI to use.
+ return config;
+};