Updated to Expo 49 and updated other packes as well. Added eslint, still need to clean up linting errors. Fix bugs in the game; uses safe area, floor is now visible, score is now visible. Typed out globals. Added eas.json and updated build scripts.

This commit is contained in:
Josh Guyette 2024-01-09 02:26:26 -06:00
parent de0f8a6311
commit 1ca88b176c
17 changed files with 411 additions and 252 deletions

30
.eslintrc.js Normal file
View File

@ -0,0 +1,30 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
],
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint", "react"],
rules: {},
ignorePatterns: ["*.d.ts"],
};

6
.gitignore vendored
View File

@ -9,6 +9,12 @@ npm-debug.*
*.mobileprovision
*.orig.*
web-build/
*.log
*.app
*.tar.gz
*.ipa
*.apk
*.aab
# macOS
.DS_Store

25
App.tsx
View File

@ -1,15 +1,36 @@
import React from "react";
import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Logs } from 'expo'
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import AppView from "./AppView";
import GameEngine from "./game/systems/GameEngine";
export default function App() {
Logs.enableExpoCliLogging()
return (
<>
<StatusBar backgroundColor="orange" hidden={true} />
<SafeAreaProvider>
<AppView />
<SafeView />
</SafeAreaProvider>
</>
);
}
function SafeView() {
const { top, bottom, left, right } = useSafeAreaInsets();
const topInset = top.valueOf();
global.topInset = topInset;
global.bottomInset = bottom;
global.leftInset = left;
global.rightInset = right;
return (
<SafeAreaView>
<GameEngine />
</SafeAreaView>
);
}

View File

@ -1,94 +0,0 @@
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;
}
case "subtractFromScore": {
setScore(score => score - 1);
break;
}
}
}}
style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }}
>
<TouchableOpacity
onPress={() => {
// gameEngine?.swap(entities());
if (isRunning) {
gameEngine?.stop();
setIsRunning(false);
} else {
gameEngine?.start();
setIsRunning(true);
}
}}
style={{
position: "absolute",
top: 0,
left: 0,
height: 55,
width: "100%",
// borderColor: "red",
// borderWidth: 1,
}}
>
<Text
style={{
textAlign: "center",
fontSize: 40,
fontWeight: "bold",
marginTop: topInset / 2,
}}
>
{isRunning ? score : "Press to Resume"}
</Text>
</TouchableOpacity>
</GameEngine>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

View File

@ -18,7 +18,8 @@
"**/*"
],
"ios": {
"supportsTablet": true
"supportsTablet": true,
"bundleIdentifier": "com.nightness.reactnativegameengineexpotypescripttemplate"
},
"android": {
"adaptiveIcon": {
@ -28,6 +29,12 @@
},
"web": {
"favicon": "./assets/favicon.png"
},
"jsEngine": "jsc",
"extra": {
"eas": {
"projectId": "bb283987-7da9-4a8e-b730-4aebcd816864"
}
}
}
}

View File

@ -20,6 +20,5 @@ export const BalloonSVG = (props) => {
<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>
)
);
};

62
eas.json Normal file
View File

@ -0,0 +1,62 @@
{
"cli": {
"version": ">= 2.7.1"
},
"build": {
"local": {
"distribution": "internal",
"android": {
"buildType": "apk",
"image": "latest"
},
"ios": {
"simulator": true
}
},
"development": {
"developmentClient": true,
"distribution": "internal",
"android": {
"buildType": "apk",
"image": "latest"
},
"ios": {
"simulator": true,
"resourceClass": "m-medium"
}
},
"preview": {
"channel": "preview",
"android": {
"buildType": "app-bundle",
"image": "latest"
},
"ios": {
"resourceClass": "m-medium"
}
},
"production": {
"channel": "production",
"android": {
"buildType": "app-bundle",
"image": "latest"
},
"ios": {
"resourceClass": "m-medium"
}
}
},
"submit": {
"preview": {
"android": {
"releaseStatus": "draft"
}
},
"production": {
"android": {
"track": "production",
"releaseStatus": "completed"
}
}
}
}

View File

@ -1,61 +1,53 @@
import React from 'react'
import Matter from 'matter-js'
import { ColorValue, View } from 'react-native'
import { BalloonSVG } from '@svg'
import { Position2D, Size2D } from '@types'
import { GameEntity, 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>
);
const Balloon = ({ body, color }: GameEntity) => {
const heightBody = body.bounds.max.y - body.bounds.min.y;
const widthBody = body.bounds.max.x - body.bounds.min.x;
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,
justifyContent: 'center',
alignItems: 'center',
}}
>
<BalloonSVG color={color} />
</View>
);
};
export default (
label: string,
world: Matter.Composite,
color: ColorValue,
pos: Position2D,
size: Size2D
) => {
const body = Matter.Bodies.rectangle(pos.x, pos.y, size.width, size.height, {
label,
isStatic: false,
id: 5,
frictionAir: 0.5,
});
Matter.Composite.add(world, body);
return {
body,
color,
pos,
renderer: Balloon,
};
export default (
label: string,
world: Matter.Composite,
color: ColorValue,
pos: Position2D,
size: Size2D
) => {
const body = Matter.Bodies.circle(pos.x, pos.y, 50, {
label,
isStatic: false,
id: 5,
// restitution: 0.4,
// friction: 1,
frictionAir: 0.5,
// 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,
};
};
};

View File

@ -2,9 +2,9 @@ import Matter from 'matter-js'
import React from 'react'
import { ColorValue, View } from 'react-native'
import { Position2D, Size2D } from '@types'
import { GameEntity, Position2D, Size2D } from '@types'
const Wall = ({ body, color }: any) => {
const Wall = ({ body, color }: GameEntity) => {
const widthBody = body.bounds.max.x - body.bounds.min.x
const heightBody = body.bounds.max.y - body.bounds.min.y

View File

@ -1,17 +1,22 @@
import { Dimensions } from "react-native";
import Matter from "matter-js";
import { windowHeight, windowWidth } from "@game";
import { Balloon, Wall } from ".";
import { GameEngineEntities } from "@types";
export const entities = (restart: boolean = false) => {
let engine = Matter.Engine.create(undefined, {
export function entities(): GameEngineEntities {
const engine = Matter.Engine.create({
enableSleeping: false,
gravity: { x: 0, y: 0.001 },
gravity: { x: 0, y: 1.75 },
} as Matter.IEngineDefinition);
let world = engine.world;
const topInset = (global as any).topInset; // for notch handling
const world = engine.world;
const [top, bottom] = [
global.topInset,
global.bottomInset,
global.leftInset,
global.rightInset,
];
const newBalloon = () =>
Balloon(
@ -20,41 +25,41 @@ export const entities = (restart: boolean = false) => {
"red",
{
x: Math.random() * (windowWidth - 100) + 50,
y: 150,
y: 200,
},
{ width: 50, height: 50 }
{ width: 40, height: 50 }
);
let entities = {
const entities = {
physics: { engine, world },
Balloon: newBalloon(),
LeftWall: Wall(
"LeftWall",
world,
"orange",
{ x: 0 - 25, y: windowHeight / 2 },
{ height: windowHeight, width: 50 }
),
RightWall: Wall(
"RightWall",
world,
"orange",
{ x: windowWidth + 25, y: windowHeight / 2 },
{ height: windowHeight, width: 50 }
),
// LeftWall: Wall(
// "LeftWall",
// world,
// "orange",
// { x: 0 - 25, y: windowHeight / 2 },
// { height: windowHeight - topInset - bottomInset + 560, width: 50 }
// ),
// RightWall: Wall(
// "RightWall",
// world,
// "orange",
// { x: windowWidth + 25, y: windowHeight / 2 },
// { height: windowHeight - topInset - bottomInset + 560, width: 50 }
// ),
Ceiling: Wall(
"Ceiling",
world,
"orange",
{ x: 0, y: 0 },
{ height: 110 + topInset, width: windowWidth * 2 }
{ height: 110 + top, width: windowWidth * 2 }
),
Floor: Wall(
"Floor",
world,
"orange",
{ x: windowWidth / 2, y: windowHeight + 60 },
{ height: 60, width: windowWidth }
{ x: windowWidth / 2, y: windowHeight - top },
{ height: 60 + bottom, width: windowWidth }
),
};
@ -72,23 +77,25 @@ export const entities = (restart: boolean = false) => {
Matter.Events.on(
engine,
"collisionStart",
({ pairs, name, source, timestamp }: Matter.IEventCollision<any>) => {
for (var i = 0, j = pairs.length; i != j; ++i) {
({ pairs }: Matter.IEventCollision<object>) => {
for (let i = 0, j = pairs.length; i != j; ++i) {
const bodyA = pairs[i].bodyA;
const bodyB = pairs[i].bodyB;
// We only want collisions between the balloon and the floor
if ((bodyA.label !== "Balloon" && bodyB.label !== "Balloon") || (bodyA.label !== "Floor" && bodyB.label !== "Floor")) {
if (
(bodyA.label !== "Balloon" && bodyB.label !== "Balloon") ||
(bodyA.label !== "Floor" && bodyB.label !== "Floor")
) {
continue;
}
const balloonBody = entities.Balloon.body;
const floorBody = entities.Floor.body;
// Remove balloon if it hits the floor
Matter.World.remove(world, balloonBody, true);
// Subtract a point from the score
const gameEngine = (global as any).gameEngine;
const gameEngine = global.gameEngine!;
gameEngine.dispatch({
type: "subtractFromScore",
});
@ -102,7 +109,7 @@ export const entities = (restart: boolean = false) => {
);
return entities;
};
}
export const useEntities = () => {
return {

View File

@ -1,13 +1,8 @@
import { Dimensions } from "react-native";
export { windowHeight, windowWidth } from "@game";
import Wall from "./Wall";
import Balloon from './Balloon';
import Balloon from "./Balloon";
export {
Wall,
Balloon
};
export { Wall, Balloon };
export * from './entities';
export * from "./entities";

11
game/global.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import { IGameEngine } from "@types";
declare global {
var topInset: number;
var bottomInset: number;
var leftInset: number;
var rightInset: number;
var gameEngine: IGameEngine | null;
}
export {};

View File

@ -0,0 +1,78 @@
import React, { useEffect, useState } from "react";
import { Text, TouchableOpacity } from "react-native";
import { GameEngine as ReactGameEngine } from "react-native-game-engine";
import { entities } from "@entities";
import { GameLoop } from "@systems";
import { GameEngineEvent, IGameEngine } from "@types";
export default function GameEngine() {
const [isRunning, setIsRunning] = React.useState(true);
const [score, setScore] = useState(0);
const [gameEngine, setGameEngine] = useState<IGameEngine | null>(null);
const [top, bottom, left, right] = [global.topInset, global.bottomInset, global.leftInset, global.rightInset]
const gameEntities = entities();
// const { engine, world } = gameEntities.physics;
// const gameEntities = entities();
useEffect(() => {
if (gameEngine) {
gameEngine.swap(entities());
}
global.gameEngine = gameEngine;
}, [gameEngine]);
return (
<ReactGameEngine
ref={(ref) => setGameEngine(ref as IGameEngine)}
systems={[GameLoop]}
entities={gameEntities}
running={isRunning}
onEvent={({ type }: GameEngineEvent) => {
switch (type) {
case "addToScore": {
setScore(score => score + 1);
break;
}
case "subtractFromScore": {
setScore(score => score - 1);
break;
}
}
}}
style={{ position: "absolute", top, left, right, bottom }}
>
<TouchableOpacity
onPress={() => {
// gameEngine?.swap(entities());
if (isRunning) {
gameEngine?.stop();
setIsRunning(false);
} else {
gameEngine?.start();
setIsRunning(true);
}
}}
style={{
position: "absolute",
top: 0,
left: 0,
height: 55,
width: "100%",
}}
>
<Text
style={{
textAlign: "center",
fontSize: 40,
fontWeight: "bold",
marginTop: 10,
}}
>
{isRunning ? score : "Press to Resume"}
</Text>
</TouchableOpacity>
</ReactGameEngine>
);
}

View File

@ -1,26 +1,27 @@
import Matter, { Vector } from "matter-js";
import { GameEngineEntities, GameEntity } from "@types";
import Matter from "matter-js";
import {
GameEngineUpdateEventOptionType,
TouchEvent,
} from "react-native-game-engine";
import { windowHeight, windowWidth } from "@game";
export const GameLoop = (
entities: any,
entities: GameEngineEntities,
{ touches, time, dispatch }: GameEngineUpdateEventOptionType
) => {
let engine = entities.physics.engine;
let world = entities.physics.world;
const engine = entities.physics.engine;
touches
.filter((t: TouchEvent) => t.type === "press")
.forEach((t: TouchEvent) => {
const balloonBody = entities.Balloon.body as Matter.IBodyDefinition;
const balloonBody = (entities.Balloon as GameEntity).body;
const balloonPos = balloonBody.position as Matter.Vector;
const { pageX, pageY } = t.event;
// if (locationX < 50 && locationY < 50) { // for some reason this works, but the line below is more readable.
if (Math.abs(pageX - balloonPos.x) < 50 && Math.abs(pageY - balloonPos.y) < 50) {
if (
Math.abs(pageX - balloonPos.x) < 100 &&
Math.abs(pageY - balloonPos.y) < 100
) {
dispatch({
type: "addToScore",
});

View File

@ -1,18 +1,39 @@
import { ColorValue } from "react-native";
import { GameEngine } from "react-native-game-engine";
export interface Position2D {
x: number;
y: number;
x: number;
y: number;
}
export interface Size2D {
width: number;
height: number;
width: number;
height: number;
}
export interface IGameEngine extends GameEngine {
stop: () => void;
start: () => void;
swap: (newEntities: Promise<any> | any) => void;
dispatch: (event: string) => void;
}
export interface IGameEngine<T = never> extends GameEngine {
stop: () => void;
start: () => void;
swap: (newEntities: Promise<GameEngineEntities> | GameEngineEntities) => void;
dispatch: (event: GameEngineEvent<T>) => void;
}
export interface GameEngineEvent<T = never> {
type: string;
[key: string]: T | string;
}
export interface GameEntity {
body: Matter.Body;
color: ColorValue;
pos: Position2D;
renderer: React.ComponentType<GameEntity>;
}
export interface GameEngineEntities {
physics: {
engine: Matter.Engine;
world: Matter.World;
};
[key: string]: GameEntity | { engine: Matter.Engine; world: Matter.World };
}

View File

@ -3,34 +3,55 @@
"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"
"start": "npx expo start --dev-client",
"android": "npx expo start --android",
"ios": "npx expo start --ios",
"web": "npx expo start --web",
"build:local": "eas build --profile local --local",
"build:dev": "eas build --profile development --local",
"build:preview": "node scripts/updateVersion.js && eas build --profile preview --local",
"build:production": "node scripts/updateVersion.js && eas build --profile production --local",
"eas:preview": "eas build --profile preview --auto-submit",
"eas:production": "eas build --profile production --auto-submit",
"submit:preview": "eas submit --profile preview",
"submit:production": "eas submit --profile production",
"update": "eas update --auto",
"update:preview": "eas update --branch preview --message \"Updating Preview\"",
"update:production": "eas update --branch production --message \"Updating Production\"",
"pod:update": "pod repo update"
},
"dependencies": {
"expo": "^47.0.0",
"expo-status-bar": "~1.4.2",
"expo": "^49.0.21",
"expo-dev-client": "~2.4.12",
"expo-status-bar": "~1.6.0",
"matter-js": "^0.18.0",
"react": "18.1.0",
"react-dom": "18.1.0",
"react-native": "0.70.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.72.6",
"react-native-game-engine": "^1.2.0",
"react-native-reanimated": "~2.12.0",
"react-native-safe-area-context": "4.4.1",
"react-native-svg": "13.4.0",
"react-native-web": "~0.18.7"
"react-native-reanimated": "~3.3.0",
"react-native-safe-area-context": "4.6.3",
"react-native-svg": "13.9.0",
"react-native-web": "~0.19.6"
},
"devDependencies": {
"@babel/core": "^7.19.3",
"@babel/plugin-proposal-class-properties": "^7.17.12",
"@babel/preset-env": "^7.17.12",
"@expo/webpack-config": "^0.17.2",
"@types/matter-js": "^0.17.7",
"@types/react": "~18.0.24",
"@types/react-native": "~0.70.6",
"typescript": "^4.6.3"
"@babel/core": "^7.23.7",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0-0",
"@babel/plugin-proposal-optional-chaining": "^7.0.0-0",
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
"@babel/plugin-transform-shorthand-properties": "^7.0.0-0",
"@babel/plugin-transform-template-literals": "^7.0.0-0",
"@babel/preset-env": "^7.23.7",
"@expo/webpack-config": "^19.0.0",
"@types/matter-js": "^0.19.5",
"@types/react": "^18.2.47",
"@types/react-native": "^0.73.0",
"@typescript-eslint/eslint-plugin": "^6.18.0",
"@typescript-eslint/parser": "^6.18.0",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2",
"typescript": "^5.3.3"
},
"license": "MIT",
"peerDependencies": {

View File

@ -1,17 +1,19 @@
{
"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"],
}
}
"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"]
}
},
"include": ["**/*", "game/global.d.ts"],
"exclude": ["node_modules", "**/*.spec.ts"]
}