initial commit

This commit is contained in:
Josh Guyette 2022-06-25 00:29:43 -05:00
parent 01434a0e75
commit 55fdfcb7d7
24 changed files with 587 additions and 0 deletions

4
.expo-shared/assets.json Normal file
View File

@ -0,0 +1,4 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}

17
.gitignore vendored Normal file
View File

@ -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

15
App.tsx Normal file
View File

@ -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 (
<>
<StatusBar backgroundColor="orange" hidden={true} />
<SafeAreaProvider>
<AppView />
</SafeAreaProvider>
</>
);
}

74
AppView.tsx Normal file
View File

@ -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<IGameEngine | null>(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 (
<SafeAreaView edges={['top']} style={[styles.container, {
}]}>
{/* @ts-ignore */}
<GameEngine
ref={(ref) => 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 }}
>
<TouchableOpacity
onPress={() => {
gameEngine?.swap(entities());
}}
>
<Text
style={{
textAlign: "center",
fontSize: 40,
fontWeight: "bold",
marginTop: topInset / 2,
}}
>
{score}
</Text>
</TouchableOpacity>
</GameEngine>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

33
app.json Normal file
View File

@ -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"
}
}
}

25
assets/SVG/BalloonSVG.js Normal file
View File

@ -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 (
<Svg
xmlns="http://www.w3.org/2000/svg"
width={50}
height={50}
viewBox="0 0 950.000000 1280.000000"
preserveAspectRatio="xMidYMid meet"
{...props}
>
<G
transform="translate(0.000000,1280.000000) scale(0.100000,-0.100000)"
fill={color}
stroke="none"
>
<Path d="M4430 12789 c-921 -59 -1769 -370 -2480 -908 -1067 -807 -1758 -2084 -1914 -3536 -69 -641 -34 -1378 100 -2105 192 -1039 624 -2084 1222 -2955 536 -781 1225 -1439 1919 -1834 140 -79 424 -214 558 -264 215 -80 439 -136 648 -162 60 -7 111 -16 115 -19 8 -9 -15 -237 -34 -326 -35 -167 -134 -411 -198 -487 -33 -40 -33 -73 2 -105 59 -57 173 -82 372 -83 213 0 331 25 393 83 36 34 34 62 -7 116 -114 150 -225 522 -226 758 0 54 -15 47 150 68 889 116 1919 814 2751 1865 480 607 892 1332 1182 2085 466 1210 624 2521 441 3659 -214 1337 -881 2491 -1874 3242 -876 663 -1971 982 -3120 908z" />
</G>
</Svg>
)
};

1
assets/SVG/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './BalloonSVG';

BIN
assets/adaptive-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

22
babel.config.js Normal file
View File

@ -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',
},
},
],
],
};
};

60
game/entities/Balloon.tsx Normal file
View File

@ -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 (
<View
style={{
position: "absolute",
left: xBody,
top: yBody,
width: widthBody,
height: heightBody,
}}
>
<BalloonSVG color={color} />
</View>
);
};
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,
};
};

26
game/entities/Finger.tsx Normal file
View File

@ -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 (
<View style={[styles.finger, { left: x, top: y }]} />
)
};
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;

45
game/entities/Wall.tsx Normal file
View File

@ -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 (
<View style={{
backgroundColor: color,
position: 'absolute',
left: xBody,
top: yBody,
width: widthBody,
height: heightBody
}} />
)
}
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
}
}

101
game/entities/entities.ts Normal file
View File

@ -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<any>) => {
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
}
}

16
game/entities/index.ts Normal file
View File

@ -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';

4
game/index.ts Normal file
View File

@ -0,0 +1,4 @@
import { Dimensions } from "react-native";
export const windowHeight = Dimensions.get("window").height;
export const windowWidth = Dimensions.get("window").width;

23
game/systems/GameLoop.ts Normal file
View File

@ -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;
};

2
game/systems/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './GameLoop';

18
game/types.ts Normal file
View File

@ -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> | any) => void;
dispatch: (event: string) => void;
}

38
package.json Normal file
View File

@ -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"
}
}

17
tsconfig.json Normal file
View File

@ -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"],
}
}
}

46
webpack.config.js Normal file
View File

@ -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;
};