用React Native制作一個簡單的游戲引擎
簡介
今天我們將學習如何使用React Native制作一個游戲。因為我們使用的是React Native,這個游戲?qū)⑹强缙脚_的,這意味著你可以在Android、iOS和網(wǎng)絡上玩同一個游戲。然而,今天我們將只關注移動設備。所以我們開始吧。
開始吧
要制作任何游戲,我們需要一個循環(huán),在我們玩的時候更新我們的游戲。這個循環(huán)被優(yōu)化以順利運行游戲,為此我們將使用 React Native游戲引擎 。
首先讓我們用以下命令創(chuàng)建一個新的React Native應用。
npx react-native init ReactNativeGame
創(chuàng)建項目后,我們需要添加一個依賴項,以便添加游戲引擎。
npm i -S react-native-game-engine
這個命令將把React Native游戲引擎添加到我們的項目中。
那么,我們要做一個什么樣的游戲呢?為了簡單起見,讓我們做一個蛇的游戲,它可以吃食物的碎片并增長身長。
對React Native游戲引擎的簡單介紹
React Native Game Engine是一個輕量級的游戲引擎。它包括一個組件,允許我們將對象的數(shù)組添加為實體,這樣我們就可以對它們進行操作。為了編寫我們的游戲邏輯,我們使用了一個系統(tǒng)道具陣列,它允許我們操縱實體(游戲?qū)ο螅?,檢測觸摸,以及許多其他令人敬畏的細節(jié),幫助我們制作一個簡單的、功能性的游戲。
讓我們在React Native中建立一個蛇形游戲
要制作一個游戲,我們需要一個畫布或容器,我們將在其中添加游戲?qū)ο?。要制作一個畫布,我們只需添加一個帶有風格的視圖組件,像這樣。
// App.js
<View style={styles.canvas}>
</View>
我們可以像這樣添加我們的樣式。
const styles = StyleSheet.create({
canvas: {
flex: 1,
backgroundColor: "#000000",
alignItems: "center",
justifyContent: "center",
}
});
在畫布中,我們將使用 GameEngine 組件和一些來自React Native Game Engine的樣式。
import { GameEngine } from "react-native-game-engine";
import React, { useRef } from "react";
import Constants from "./Constants";
export default function App() {
const BoardSize = Constants.GRID_SIZE * Constants.CELL_SIZE;
const engine = useRef(null);
return (
<View style={styles.canvas}>
<GameEngine
ref={engine}
style={{
width: BoardSize,
height: BoardSize,
flex: null,
backgroundColor: "white",
}}
/>
</View>
);
我們還使用 useRef() React Hook為游戲引擎添加了一個ref,以便日后使用。
我們還在項目的根部創(chuàng)建了一個 Constants.js 文件來存儲我們的常量值。
// Constants.js
import { Dimensions } from "react-native";
export default {
MAX_WIDTH: Dimensions.get("screen").width,
MAX_HEIGHT: Dimensions.get("screen").height,
GRID_SIZE: 15,
CELL_SIZE: 20
};
你會注意到我們正在做一個15乘15的網(wǎng)格,我們的蛇將在那里移動。
這時我們的游戲引擎已經(jīng)設置好了,以顯示蛇和它的食物。我們需要將實體和道具添加到 GameEngine ,但在此之前,我們需要創(chuàng)建一個蛇和食物的組件,在設備上渲染。
創(chuàng)建游戲?qū)嶓w
讓我們首先制作蛇。蛇分為兩部分,頭部和身體(或尾巴)?,F(xiàn)在我們將制作蛇的頭部,我們將在本教程的后面添加蛇的尾巴。
為了制作蛇的頭部,我們將在組件文件夾中制作一個 Head 組件。
正如你所看到的,我們有三個組件: Head , Food ,和 Tail 。我們將在本教程中逐一查看這些文件的內(nèi)容。
在 Head 組件中,我們將創(chuàng)建一個帶有一些樣式的視圖。
import React from "react";
import { View } from "react-native";
export default function Head({ position, size }) {
return (
<View
style={{
width: size,
height: size,
backgroundColor: "red",
position: "absolute",
left: position[0] * size,
top: position[1] * size,
}}
></View>
);
}
我們將傳遞一些道具來設置頭部的大小和位置。
我們使用 position: "absolute" 屬性來輕松移動頭部。
這將呈現(xiàn)一個正方形,我們不打算使用更復雜的東西;一個正方形或長方形的形狀代表蛇的身體,一個圓形的形狀代表食物。
現(xiàn)在讓我們將這條蛇的頭部添加到 GameEngine 。
要添加任何實體,我們需要在 GameEngine 中的 entities 道具中傳遞一個對象。
//App.js
import Head from "./components/Head";
<GameEngine
ref={engine}
style={{
width: BoardSize,
height: BoardSize,
flex: null,
backgroundColor: "white",
}}
entities={{
head: {
position: [0, 0],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Head />,
}
}}
/>
我們在 entities 道具中傳遞了一個對象,其關鍵是頭。這些是它定義的屬性。
position是一組坐標,用于放置蛇頭。size是設置蛇頭大小的值。xspeed和yspeed是決定蛇的運動和方向的值,可以是1、0或-1。注意,當 xspeed 被設置為1或-1時,那么 yspeed 的值必須為0,反之亦然- 最后,
renderer,負責渲染該組件 updateFrequency和nextMove將在后面討論。
在添加完 Head 組件后,我們也來添加其他組件。
// commponets/Food/index.js
import React from "react";
import { View } from "react-native";
export default function Food({ position, size }) {
return (
<View
style={{
width: size,
height: size,
backgroundColor: "green",
position: "absolute",
left: position[0] * size,
top: position[1] * size,
borderRadius: 50
}}
></View>
);
}
Food 組件與 Head 組件類似,但我們改變了背景顏色和邊框半徑,使其成為一個圓形。
現(xiàn)在創(chuàng)建一個 Tail 組件。這個可能很棘手。
// components/Tail/index.js
import React from "react";
import { View } from "react-native";
import Constants from "../../Constants";
export default function Tail({ elements, position, size }) {
const tailList = elements.map((el, idx) => (
<View
key={idx}
style={{
width: size,
height: size,
position: "absolute",
left: el[0] * size,
top: el[1] * size,
backgroundColor: "red",
}}
/>
));
return (
<View
style={{
width: Constants.GRID_SIZE * size,
height: Constants.GRID_SIZE * size,
}}
>
{tailList}
</View>
);
}
當蛇吃了食物后,我們將在蛇身中添加一個元素,這樣我們的蛇就會成長。這些元素將傳入 Tail 組件,這將表明它必須變大。
我們將循環(huán)瀏覽所有的元素來創(chuàng)建整個蛇身,附加上它,然后渲染。
在制作完所有需要的組件后,讓我們把這兩個組件作為 GameEngine 。
// App.js
import Food from "./components/Food";
import Tail from "./components/Tail";
// App.js
const randomPositions = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
// App.js
<GameEngine
ref={engine}
style={{
width: BoardSize,
height: BoardSize,
flex: null,
backgroundColor: "white",
}}
entities={{
head: {
position: [0, 0],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Head />,
},
food: {
position: [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
],
size: Constants.CELL_SIZE,
renderer: <Food />,
},
tail: {
size: Constants.CELL_SIZE,
elements: [],
renderer: <Tail />,
},
}}
/>
為了保證食物位置的隨機性,我們做了一個帶有最小和最大參數(shù)的 randomPositions 函數(shù)。
在 tail ,我們在初始狀態(tài)下添加了一個空數(shù)組,所以當蛇吃到食物時,它將在 elements: 空間中存儲每個尾巴的長度。
在這一點上,我們已經(jīng)成功創(chuàng)建了我們的游戲組件?,F(xiàn)在是在游戲循環(huán)中添加游戲邏輯的時候了。
游戲邏輯
為了使游戲循環(huán), GameEngine 組件有一個叫 systems 的道具,它接受一個數(shù)組的函數(shù)。
為了保持一切結(jié)構(gòu)化,我正在創(chuàng)建一個名為 systems 的文件夾,并插入一個名為 GameLoop.js 的文件。
在這個文件中,我們正在導出一個帶有某些參數(shù)的函數(shù)。
// GameLoop.js
export default function (entities, { events, dispatch }) {
...
return entities;
}
第一個參數(shù)是 entities ,它包含了我們傳遞給 GameEngine 組件的所有實體,所以我們可以操作它們。另一個參數(shù)是一個帶有屬性的對象,即 events 和 dispatch 。
移動蛇頭
讓我們編寫代碼,將蛇頭向正確的方向移動。
在 GameLoop.js 函數(shù)中,我們將更新頭部的位置,因為這個函數(shù)在每一幀都會被調(diào)用。
// GameLoop.js
export default function (entities, { events, dispatch }) {
const head = entities.head;
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
}
我們使用 entities 參數(shù)訪問頭部,在每一幀中我們都要更新蛇頭的位置。
如果你現(xiàn)在玩游戲,什么也不會發(fā)生,因為我們把 xspeed 和 yspeed 設置為0。如果你把 xspeed 或 yspeed 設置為1,蛇的頭部會移動得很快。
為了減慢蛇的速度,我們將像這樣使用 nextMove 和 updateFrequency 的值。
const head = entities.head;
head.nextMove -= 1;
if (head.nextMove === 0) {
head.nextMove = head.updateFrequency;
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
}
我們通過在每一幀中減去1來更新 nextMove 的值為0。當值為0時, if 條件被設置為 true , nextMove 值被更新回初始值,從而移動蛇的頭部。
現(xiàn)在,蛇的速度應該比以前慢了。
"游戲結(jié)束!"條件
在這一點上,我們還沒有添加 "游戲結(jié)束!"條件。第一個 "游戲結(jié)束!"條件是當蛇碰到墻時,游戲停止運行,并向用戶顯示一條信息,表明游戲已經(jīng)結(jié)束。
為了添加這個條件,我們使用這段代碼。
if (head.nextMove === 0) {
head.nextMove = head.updateFrequency;
if (
head.position[0] + head.xspeed < 0 ||
head.position[0] + head.xspeed >= Constants.GRID_SIZE ||
head.position[1] + head.yspeed < 0 ||
head.position[1] + head.yspeed >= Constants.GRID_SIZE
) {
dispatch("game-over");
} else {
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
}
第二個 if 條件是檢查蛇頭是否觸及墻壁。如果該條件為真,那么我們將使用 dispatch 函數(shù)來發(fā)送一個 "game-over" 事件。
通過 else ,我們正在更新蛇的頭部位置。
現(xiàn)在讓我們添加 "游戲結(jié)束!"的功能。
每當我們派發(fā)一個 "game-over" 事件時,我們將停止游戲,并顯示一個警告:"游戲結(jié)束!"讓我們來實現(xiàn)它。
為了監(jiān)聽 "game-over" 事件,我們需要將 onEvent 道具傳遞給 GameEngine 組件。為了停止游戲,我們需要添加一個 running 道具并傳入 useState 。
我們的 GameEngine 應該看起來像這樣。
// App.js
import React, { useRef, useState } from "react";
import GameLoop from "./systems/GameLoop";
....
....
const [isGameRunning, setIsGameRunning] = useState(true);
....
....
<GameEngine
ref={engine}
style={{
width: BoardSize,
height: BoardSize,
flex: null,
backgroundColor: "white",
}}
entities={{
head: {
position: [0, 0],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Head />,
},
food: {
position: [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
],
size: Constants.CELL_SIZE,
renderer: <Food />,
},
tail: {
size: Constants.CELL_SIZE,
elements: [],
renderer: <Tail />,
},
}}
systems={[GameLoop]}
running={isGameRunning}
onEvent={(e) => {
switch (e) {
case "game-over":
alert("Game over!");
setIsGameRunning(false);
return;
}
}}
/>
在 GameEngine 中,我們已經(jīng)添加了 systems 道具,并通過我們的 GameLoop 函數(shù)傳入了一個數(shù)組,同時還有一個 running 道具和一個 isGameRunning 狀態(tài)。最后,我們添加了 onEvent 道具,它接受一個帶有事件參數(shù)的函數(shù),這樣我們就可以監(jiān)聽我們的事件。
在這種情況下,我們在switch語句中監(jiān)聽 "game-over" 事件,所以當我們收到該事件時,我們顯示 "Game over!" 警報,并將 isGameRunning 狀態(tài)設置為 false ,以停止游戲。
食用食物
我們已經(jīng)寫好了 "游戲結(jié)束!"的邏輯,現(xiàn)在讓我們來寫一下讓蛇吃食物的邏輯。
當蛇吃了食物后,食物的位置應該隨機變化。
打開 GameLoop.js ,寫下以下代碼。
// GameLoop.js
const randomPositions = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
export default function (entities, { events, dispatch }) {
const head = entities.head;
const food = entities.food;
....
....
....
if (
head.position[0] + head.xspeed < 0 ||
head.position[0] + head.xspeed >= Constants.GRID_SIZE ||
head.position[1] + head.yspeed < 0 ||
head.position[1] + head.yspeed >= Constants.GRID_SIZE
) {
dispatch("game-over");
} else {
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
if (
head.position[0] == food.position[0] &&
head.position[1] == food.position[1]
) {
food.position = [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
];
}
}
我們添加了一個 if ,以檢查蛇頭和食物的位置是否相同(這將表明蛇已經(jīng) "吃 "了食物)。然后,我們使用 randomPositions 函數(shù)更新食物的位置,正如我們在上面的 App.js 。請注意,我們是通過 entities 參數(shù)來訪問食物的。
控制蛇
現(xiàn)在讓我們來添加蛇的控制。我們將使用按鈕來控制蛇的移動位置。
要做到這一點,我們需要在畫布下面的屏幕上添加按鈕。
// App.js
import React, { useRef, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import { GameEngine } from "react-native-game-engine";
import { TouchableOpacity } from "react-native-gesture-handler";
import Food from "./components/Food";
import Head from "./components/Head";
import Tail from "./components/Tail";
import Constants from "./Constants";
import GameLoop from "./systems/GameLoop";
export default function App() {
const BoardSize = Constants.GRID_SIZE * Constants.CELL_SIZE;
const engine = useRef(null);
const [isGameRunning, setIsGameRunning] = useState(true);
const randomPositions = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
const resetGame = () => {
engine.current.swap({
head: {
position: [0, 0],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Head />,
},
food: {
position: [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Food />,
},
tail: {
size: Constants.CELL_SIZE,
elements: [],
renderer: <Tail />,
},
});
setIsGameRunning(true);
};
return (
<View style={styles.canvas}>
<GameEngine
ref={engine}
style={{
width: BoardSize,
height: BoardSize,
flex: null,
backgroundColor: "white",
}}
entities={{
head: {
position: [0, 0],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Head />,
},
food: {
position: [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
],
size: Constants.CELL_SIZE,
renderer: <Food />,
},
tail: {
size: Constants.CELL_SIZE,
elements: [],
renderer: <Tail />,
},
}}
systems={[GameLoop]}
running={isGameRunning}
onEvent={(e) => {
switch (e) {
case "game-over":
alert("Game over!");
setIsGameRunning(false);
return;
}
}}
/>
<View style={styles.controlContainer}>
<View style={styles.controllerRow}>
<TouchableOpacity onPress={() => engine.current.dispatch("move-up")}>
<View style={styles.controlBtn} />
</TouchableOpacity>
</View>
<View style={styles.controllerRow}>
<TouchableOpacity
onPress={() => engine.current.dispatch("move-left")}
>
<View style={styles.controlBtn} />
</TouchableOpacity>
<View style={[styles.controlBtn, { backgroundColor: null }]} />
<TouchableOpacity
onPress={() => engine.current.dispatch("move-right")}
>
<View style={styles.controlBtn} />
</TouchableOpacity>
</View>
<View style={styles.controllerRow}>
<TouchableOpacity
onPress={() => engine.current.dispatch("move-down")}
>
<View style={styles.controlBtn} />
</TouchableOpacity>
</View>
</View>
{!isGameRunning && (
<TouchableOpacity onPress={resetGame}>
<Text
style={{
color: "white",
marginTop: 15,
fontSize: 22,
padding: 10,
backgroundColor: "grey",
borderRadius: 10
}}
>
Start New Game
</Text>
</TouchableOpacity>
)}
</View>
);
}
const styles = StyleSheet.create({
canvas: {
flex: 1,
backgroundColor: "#000000",
alignItems: "center",
justifyContent: "center",
},
controlContainer: {
marginTop: 10,
},
controllerRow: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
controlBtn: {
backgroundColor: "yellow",
width: 100,
height: 100,
},
});
除了控制之外,我們還添加了一個按鈕,以便在前一個游戲結(jié)束時開始一個新的游戲。這個按鈕只在游戲沒有運行時出現(xiàn)。在點擊該按鈕時,我們通過使用游戲引擎的 swap 函數(shù)來重置游戲,傳入實體的初始對象,并更新游戲的運行狀態(tài)。
現(xiàn)在說說控制。我們已經(jīng)添加了可觸摸物體,當按下這些物體時,就會派發(fā)將在游戲循環(huán)中處理的事件。
// GameLoop.js
....
....
export default function (entities, { events, dispatch }) {
const head = entities.head;
const food = entities.food;
if (events.length) {
events.forEach((e) => {
switch (e) {
case "move-up":
if (head.yspeed === 1) return;
head.yspeed = -1;
head.xspeed = 0;
return;
case "move-right":
if (head.xspeed === -1) return;
head.xspeed = 1;
head.yspeed = 0;
return;
case "move-down":
if (head.yspeed === -1) return;
head.yspeed = 1;
head.xspeed = 0;
return;
case "move-left":
if (head.xspeed === 1) return;
head.xspeed = -1;
head.yspeed = 0;
return;
}
});
}
....
....
});
在上面的代碼中,我們添加了一個 switch 語句來識別事件并更新蛇的方向。
還在聽我說嗎?很好!唯一剩下的就是尾巴了。
尾巴功能
當蛇吃了食物后,我們希望它的尾巴能長出來。我們還想在蛇咬到自己的尾巴或身體時發(fā)出一個 "游戲結(jié)束!"的事件。
讓我們來添加尾巴邏輯。
// GameLoop.js
const tail = entities.tail;
....
....
....
else {
tail.elements = [[head.position[0], head.position[1]], ...tail.elements];
tail.elements.pop();
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
tail.elements.forEach((el, idx) => {
if (
head.position[0] === el[0] &&
head.position[1] === el[1]
)
dispatch("game-over");
});
if (
head.position[0] == food.position[0] &&
head.position[1] == food.position[1]
) {
tail.elements = [
[head.position[0], head.position[1]],
...tail.elements,
];
food.position = [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
];
}
}
為了使尾巴跟隨蛇的頭部,我們要更新尾巴的元素。我們通過將頭部的位置添加到元素數(shù)組的開頭,然后刪除尾巴元素數(shù)組上的最后一個元素來實現(xiàn)這一目的。
在這之后,我們寫一個條件,如果蛇咬了自己的身體,我們就分派 "game-over" 事件。
最后,每當蛇吃了食物,我們就用蛇頭的當前位置來追加蛇尾的元素,以增加蛇尾的長度。
下面是 GameLoop.js 的完整代碼。
// GameLoop.js
import Constants from "../Constants";
const randomPositions = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
export default function (entities, { events, dispatch }) {
const head = entities.head;
const food = entities.food;
const tail = entities.tail;
if (events.length) {
events.forEach((e) => {
switch (e) {
case "move-up":
if (head.yspeed === 1) return;
head.yspeed = -1;
head.xspeed = 0;
return;
case "move-right":
if (head.xspeed === -1) return;
head.xspeed = 1;
head.yspeed = 0;
// ToastAndroid.show("move right", ToastAndroid.SHORT);
return;
case "move-down":
if (head.yspeed === -1) return;
// ToastAndroid.show("move down", ToastAndroid.SHORT);
head.yspeed = 1;
head.xspeed = 0;
return;
case "move-left":
if (head.xspeed === 1) return;
head.xspeed = -1;
head.yspeed = 0;
// ToastAndroid.show("move left", ToastAndroid.SHORT);
return;
}
});
}
head.nextMove -= 1;
if (head.nextMove === 0) {
head.nextMove = head.updateFrequency;
if (
head.position[0] + head.xspeed < 0 ||
head.position[0] + head.xspeed >= Constants.GRID_SIZE ||
head.position[1] + head.yspeed < 0 ||
head.position[1] + head.yspeed >= Constants.GRID_SIZE
) {
dispatch("game-over");
} else {
tail.elements = [[head.position[0], head.position[1]], ...tail.elements];
tail.elements.pop();
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
tail.elements.forEach((el, idx) => {
console.log({ el, idx });
if (
head.position[0] === el[0] &&
head.position[1] === el[1]
)
dispatch("game-over");
});
if (
head.position[0] == food.position[0] &&
head.position[1] == food.position[1]
) {
tail.elements = [
[head.position[0], head.position[1]],
...tail.elements,
];
food.position = [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
];
}
}
}
return entities;
}
結(jié)語
現(xiàn)在你的第一個React Native游戲已經(jīng)完成了你可以在自己的設備上運行這個游戲來玩。我希望你能學到一些新的東西,也希望你能與你的朋友分享。
謝謝你的閱讀,祝你有個愉快的一天。
The post How to build a simple game in React Native appeared first onLogRocket Blog .
以上就是用React Native構(gòu)建一個簡單的游戲的詳細內(nèi)容,更多關于React Native游戲的資料請關注腳本之家其它相關文章!
相關文章
使用?React?Hooks?重構(gòu)類組件的示例詳解
這篇文章主要介紹了如何使用?React?Hooks?重構(gòu)類組件,本文就來通過一些常見示例看看如何使用 React Hooks 來重構(gòu)類組件,需要的朋友可以參考下2022-07-07
React18從0實現(xiàn)dispatch?update流程
這篇文章主要為大家介紹了React18從0實現(xiàn)dispatch?update流程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01

