├── .expo ├── README.md └── devices.json ├── .gitignore ├── .watchmanconfig ├── App.js ├── README.md ├── Soccer.js ├── app.json ├── components ├── Emoji.js └── Score.js ├── eas.json ├── images.js ├── images ├── baseball.png ├── basketball.png ├── football.png ├── golf.png ├── hockey.png ├── soccer.png ├── soccerGame.gif ├── sportballs.png └── tennis.png ├── index.android.js ├── package.json ├── tsconfig.json ├── useCachedResources.ts └── yarn.lock /.expo/README.md: -------------------------------------------------------------------------------- 1 | > Why do I have a folder named ".expo" in my project? 2 | The ".expo" folder is created when an Expo project is started using "expo start" command. 3 | > What do the files contain? 4 | - "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds. 5 | - "settings.json": contains the server configuration that is used to serve the application manifest. 6 | > Should I commit the ".expo" folder? 7 | No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine. 8 | Upon project creation, the ".expo" folder is already added to your ".gitignore" file. 9 | -------------------------------------------------------------------------------- /.expo/devices.json: -------------------------------------------------------------------------------- 1 | { 2 | "devices": [] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IJ 26 | # 27 | *.iml 28 | .idea 29 | .gradle 30 | local.properties 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | 37 | # BUCK 38 | buck-out/ 39 | \.buckd/ 40 | android/app/libs 41 | android/keystores/debug.keystore 42 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | 2 | import useCachedResources from './useCachedResources'; 3 | import React from 'react'; 4 | import Soccer from './Soccer'; 5 | 6 | 7 | export default function App() { 8 | const isLoadingComplete = useCachedResources(); 9 | 10 | if (!isLoadingComplete) { 11 | return null 12 | } else { 13 | return ( 14 | 15 | ); 16 | } 17 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-soccer 2 | React Native clone of the Facebook Messenger soccer game 3 | 4 | ![Demo GIF](/images/soccerGame.gif) 5 | 6 | ## Installation 7 | * `git clone https://github.com/nitishp/react-native-soccer` 8 | * `cd react-native-soccer` 9 | * `react-native run-android` 10 | 11 | This is my first real React Native project. Feel free to give me any feedback! 12 | -------------------------------------------------------------------------------- /Soccer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {View,Text, StyleSheet,Dimensions,Image,Modal,TouchableOpacity,TouchableWithoutFeedback} from 'react-native'; 3 | import * as Haptics from 'expo-haptics'; 4 | import Score from './components/Score'; 5 | import Emoji from './components/Emoji'; 6 | import * as All from './images'; 7 | 8 | const LC_IDLE = 0; 9 | const LC_RUNNING = 1; 10 | const LC_TAPPED = 2; 11 | const GRAVITY = 0.6; 12 | const TAPPED_VELOCITY = 20; 13 | const ROTATION_FACTOR = 7; 14 | const SCREEN_HEIGHT = Dimensions.get('window').height; 15 | const SCREEN_WIDTH = Dimensions.get('window').width; 16 | const BALL_WIDTH = SCREEN_WIDTH * 0.33; 17 | const BALL_HEIGHT = SCREEN_WIDTH * 0.33; 18 | const FLOOR_Y = SCREEN_HEIGHT - BALL_HEIGHT; 19 | const FLOOR_X = SCREEN_WIDTH / 2; 20 | const SCORE_Y = SCREEN_HEIGHT / 6; 21 | const EMOJI_Y = SCREEN_HEIGHT / 3; 22 | const sports = ['soccer', 'baseball', 'basketball', 'football', 'golf', 'tennis', 'hockey'] 23 | 24 | function Sports({sport, set}) { 25 | return ( 26 | set(sport)}> 27 | 28 | 29 | ) 30 | } 31 | 32 | class Soccer extends Component { 33 | constructor(props) { 34 | super(props); 35 | this.interval = null; 36 | this.state = { 37 | x: FLOOR_X, 38 | y: FLOOR_Y, 39 | vx: 0, 40 | vy: 0, 41 | lifeCycle: LC_IDLE, 42 | score: 0, 43 | scored: false, 44 | lost: false, 45 | rotate: 0, 46 | sport: All.soccer, 47 | visible: true, 48 | button: true 49 | }; 50 | } 51 | 52 | componentDidMount() { 53 | this.interval = setInterval(this.update.bind(this), 1000 / 60); 54 | } 55 | 56 | componentWillUnmount() { 57 | if(this.interval) { 58 | clearInterval(this.interval); 59 | } 60 | } 61 | 62 | onTap(event) { 63 | Haptics.selectionAsync() 64 | this.setState({button: false}) 65 | if(this.state.lifeCycle === LC_TAPPED) { 66 | this.setState({ 67 | lifeCycle: LC_RUNNING, 68 | scored: false, 69 | }); 70 | } 71 | else { 72 | let centerX = BALL_WIDTH / 2; 73 | let centerY = BALL_HEIGHT / 2; 74 | let velocityX = ((centerX - event.locationX) / SCREEN_WIDTH) * TAPPED_VELOCITY; 75 | let velocityY = -TAPPED_VELOCITY; 76 | this.setState({ 77 | vx: velocityX, 78 | vy: velocityY, 79 | score: this.state.score + 1, 80 | lifeCycle: LC_TAPPED, 81 | scored: true, 82 | lost: false, 83 | }); 84 | } 85 | return false; 86 | } 87 | 88 | updatePosition(nextState) { 89 | nextState.x += nextState.vx; 90 | nextState.y += nextState.vy; 91 | nextState.rotate += ROTATION_FACTOR * nextState.vx; 92 | // Hit the left wall 93 | if(nextState.x < BALL_WIDTH / 2) { 94 | Haptics.selectionAsync() 95 | nextState.vx = -nextState.vx; 96 | nextState.x = BALL_WIDTH / 2; 97 | } 98 | // Hit the right wall 99 | if(nextState.x > SCREEN_WIDTH - BALL_WIDTH / 2) { 100 | Haptics.selectionAsync() 101 | nextState.vx = -nextState.vx; 102 | nextState.x = SCREEN_WIDTH - BALL_WIDTH / 2; 103 | } 104 | // Reset after falling down 105 | if(nextState.y > SCREEN_HEIGHT + BALL_HEIGHT) { 106 | Haptics.selectionAsync() 107 | nextState.y = FLOOR_Y; 108 | nextState.x = FLOOR_X; 109 | nextState.lifeCycle = LC_IDLE; 110 | nextState.score = 0; 111 | nextState.lost = true; 112 | nextState.scored = false; 113 | } 114 | } 115 | 116 | updateVelocity(nextState) { 117 | nextState.vy += GRAVITY; 118 | } 119 | 120 | update() { 121 | if(this.state.lifeCycle === LC_IDLE) { 122 | this.setState({button: true}) 123 | return; 124 | } 125 | let nextState = Object.assign({}, this.state); 126 | this.updatePosition(nextState); 127 | this.updateVelocity(nextState); 128 | this.setState(nextState); 129 | } 130 | 131 | setSport(sport){ 132 | Haptics.selectionAsync() 133 | this.setState({visible: false, sport: All[`${sport}`]}) 134 | } 135 | 136 | render() { 137 | var position = { 138 | left: this.state.x - (BALL_WIDTH / 2), 139 | top: this.state.y - (BALL_HEIGHT / 2), 140 | } 141 | var rotation = { 142 | transform: [ 143 | {rotate: this.state.rotate + 'deg'}, 144 | ], 145 | } 146 | return ( 147 | 148 | 149 | 150 | this.onTap(event.nativeEvent)} 152 | onPressIn={(event) => this.onTap(event.nativeEvent)} 153 | onPressOut={(event) => this.onTap(event.nativeEvent)}> 154 | 155 | 156 | 157 | 158 | 159 | Choose your sport 160 | 161 | 162 | {sports.map(sport => {return this.setSport(sport)} sport={sport} /> })} 163 | 164 | 165 | 166 | {this.state.button == true && {this.setState({visible: true}), Haptics.selectionAsync()}} 167 | style={{padding: 10, height: 55, width: 55, position: 'absolute', bottom: 30, right: 30, borderColor: '#e7e7e6', borderWidth: 0.5, shadowColor: "#555a74", 168 | shadowOffset: { height: 0.5, width: 0.5 }, 169 | shadowOpacity: 0.5, 170 | shadowRadius: 0.5, 171 | backgroundColor: "white", 172 | borderRadius: 5, 173 | elevation: 8, 174 | justifyContent: 'center', 175 | alignItems: 'center'}}>} 176 | 177 | ); 178 | } 179 | } 180 | 181 | const styles = StyleSheet.create({ 182 | ball: { 183 | width: BALL_WIDTH, 184 | height: BALL_HEIGHT, 185 | }, 186 | }); 187 | 188 | export default Soccer; 189 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Tap It Up Sports", 4 | "slug": "Tap-It-Up-Sports", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./images/soccer.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./images/soccer.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true, 22 | "bundleIdentifier": "com.konjo.tapitupsports", 23 | "buildNumber": "6", 24 | "infoPlist": { 25 | "ITSAppUsesNonExemptEncryption": false 26 | } 27 | }, 28 | "android": { 29 | "versionCode": 6, 30 | "adaptiveIcon": { 31 | "foregroundImage": "./images/soccer.png", 32 | "backgroundColor": "#ffffff" 33 | }, 34 | "package": "com.konjo.tapitupsports" 35 | }, 36 | "extra": { 37 | "eas": { 38 | "projectId": "78531fa9-4fcc-44cc-a3ae-5dd1bd5b427d" 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /components/Emoji.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Text, 3 | View, 4 | StyleSheet, 5 | Dimensions, 6 | Animated} from 'react-native'; 7 | 8 | class Emoji extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.scored = ['👍', '👏', '👋', '😎', '💪']; 13 | this.missed = ['😢', '😭', '😔', '😡', '😠']; 14 | this.state = { 15 | opacity: new Animated.Value(0), 16 | }; 17 | } 18 | 19 | render() { 20 | let randomIndex = Math.floor(Math.random() * 5); 21 | let emojiChar = ""; 22 | if(this.props.lost === true) { 23 | emojiChar = this.missed[randomIndex]; 24 | } 25 | else { 26 | emojiChar = this.scored[randomIndex]; 27 | } 28 | let windowWidth = Dimensions.get('window').width; 29 | let position = { 30 | width: windowWidth, 31 | top: this.props.y, 32 | opacity: this.state.opacity, 33 | } 34 | return ( 35 | 36 | 37 | {emojiChar} 38 | 39 | 40 | ); 41 | } 42 | 43 | shouldComponentUpdate(nextProps, nextState) { 44 | return (nextProps.scored !== this.props.scored) 45 | || (nextProps.lost !== this.props.lost); 46 | } 47 | 48 | componentDidUpdate() { 49 | this.state.opacity.setValue(1); 50 | Animated.timing( 51 | this.state.opacity, 52 | { 53 | toValue: 0, 54 | duration: 1000, 55 | useNativeDriver: true 56 | } 57 | ).start(); 58 | } 59 | } 60 | 61 | const styles = StyleSheet.create({ 62 | container: { 63 | position: 'absolute', 64 | justifyContent: 'flex-start', 65 | alignItems: 'center', 66 | }, 67 | 68 | emoji: { 69 | flex: 1, 70 | fontSize: 25, 71 | } 72 | }); 73 | 74 | export default Emoji; 75 | -------------------------------------------------------------------------------- /components/Score.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {View, Text, StyleSheet, Dimensions, Animated} from 'react-native'; 3 | 4 | class Score extends Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.animationConfig = { 9 | toValue: 1.0, 10 | duration: 200, 11 | useNativeDriver: true 12 | } 13 | this.state = { 14 | bounceValue : new Animated.Value(0), 15 | } 16 | this.animation = Animated.timing(this.state.bounceValue, this.animationConfig); 17 | } 18 | 19 | render() { 20 | let windowWidth = Dimensions.get('window').width; 21 | let containerPosition = { 22 | top: this.props.y, 23 | width: windowWidth, 24 | transform: [ 25 | {scale: this.state.bounceValue}, 26 | ], 27 | } 28 | 29 | return ( 30 | 31 | 32 | {this.props.score} 33 | 34 | 35 | ); 36 | } 37 | 38 | componentDidUpdate() { 39 | this.bounce(); 40 | } 41 | 42 | shouldComponentUpdate(nextProps, nextState) { 43 | return nextProps.scored === true && nextProps.scored !== this.props.scored; 44 | } 45 | 46 | componentDidMount() { 47 | this.bounce(); 48 | } 49 | 50 | bounce() { 51 | this.state.bounceValue.setValue(0.5); 52 | this.animation.start(); 53 | } 54 | } 55 | 56 | const styles = StyleSheet.create({ 57 | container: { 58 | position: 'absolute', 59 | justifyContent: 'flex-start', 60 | alignItems: 'center', 61 | }, 62 | 63 | score: { 64 | fontSize: 100, 65 | fontWeight: '100', 66 | flex: 1, 67 | } 68 | }); 69 | 70 | export default Score; 71 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 3.1.1" 4 | }, 5 | "build": { 6 | "development": { 7 | "distribution": "internal", 8 | "android": { 9 | "gradleCommand": ":app:assembleDebug" 10 | }, 11 | "ios": { 12 | "resourceClass": "m1-medium", 13 | "buildConfiguration": "Debug" 14 | } 15 | }, 16 | "preview": { 17 | "distribution": "internal" 18 | }, 19 | "production": { 20 | "ios": { 21 | "resourceClass": "m1-medium" 22 | } 23 | } 24 | }, 25 | "submit": { 26 | "production": {} 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /images.js: -------------------------------------------------------------------------------- 1 | 2 | export const soccer = require('./images/soccer.png'); 3 | export const baseball = require('./images/baseball.png'); 4 | export const basketball = require('./images/basketball.png'); 5 | export const football = require('./images/football.png'); 6 | export const golf = require('./images/golf.png'); 7 | export const tennis = require('./images/tennis.png'); 8 | export const hockey = require('./images/hockey.png'); 9 | -------------------------------------------------------------------------------- /images/baseball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/baseball.png -------------------------------------------------------------------------------- /images/basketball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/basketball.png -------------------------------------------------------------------------------- /images/football.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/football.png -------------------------------------------------------------------------------- /images/golf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/golf.png -------------------------------------------------------------------------------- /images/hockey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/hockey.png -------------------------------------------------------------------------------- /images/soccer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/soccer.png -------------------------------------------------------------------------------- /images/soccerGame.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/soccerGame.gif -------------------------------------------------------------------------------- /images/sportballs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/sportballs.png -------------------------------------------------------------------------------- /images/tennis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/tennis.png -------------------------------------------------------------------------------- /index.android.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample React Native App 3 | * https://github.com/facebook/react-native 4 | * @flow 5 | */ 6 | 7 | import React, { Component } from 'react'; 8 | import { 9 | AppRegistry, 10 | } from 'react-native'; 11 | 12 | import Soccer from './Soccer'; 13 | 14 | AppRegistry.registerComponent('Soccer', () => Soccer); 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keepup", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@types/react": "~18.0.24", 13 | "@types/react-native": "~0.70.6", 14 | "expo": "^47.0.13", 15 | "expo-haptics": "^12.0.1", 16 | "expo-splash-screen": "^0.17.5", 17 | "metro-core": "^0.74.1", 18 | "react": "18.1.0", 19 | "react-native": "0.70.5", 20 | "typescript": "^4.6.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "extends": "expo/tsconfig.base" 4 | } 5 | -------------------------------------------------------------------------------- /useCachedResources.ts: -------------------------------------------------------------------------------- 1 | import * as SplashScreen from 'expo-splash-screen'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | export default function useCachedResources() { 5 | const [isLoadingComplete, setLoadingComplete] = useState(false); 6 | 7 | useEffect(() => { 8 | async function loadResourcesAndDataAsync() { 9 | try { 10 | SplashScreen.preventAutoHideAsync(); 11 | } catch (e) { 12 | console.warn(e); 13 | } finally { 14 | setLoadingComplete(true); 15 | SplashScreen.hideAsync(); 16 | } 17 | } 18 | 19 | loadResourcesAndDataAsync(); 20 | }, []); 21 | 22 | return isLoadingComplete; 23 | } 24 | --------------------------------------------------------------------------------