简介
今天我们将学习如何使用React Native制作一个游戏。因为我们使用的是React Native,这个游戏将是跨平台的,这意味着你可以在Android、iOS和网络上玩同一个游戏。然而,今天我们将只关注移动设备。所以我们开始吧。
开始吧
要制作任何游戏,我们需要一个循环,在我们玩的时候更新我们的游戏。这个循环被优化以顺利运行游戏,为此我们将使用 React Native游戏引擎 。
首先让我们用以下命令创建一个新的React Native应用。
npx react-native init ReactNativeGame
创建项目后,我们需要添加一个依赖项,以便添加游戏引擎。
npm i -S react-native-game-engine
这个命令将把React Native游戏引擎添加到我们的项目中。
那么,我们要做一个什么样的游戏呢?为了简单起见,让我们做一个蛇的游戏,它可以吃食物的碎片并增长身长。
对React Native游戏引擎的简单介绍
React Native Game Engine是一个轻量级的游戏引擎。它包括一个组件,允许我们将对象的数组添加为实体,这样我们就可以对它们进行操作。为了编写我们的游戏逻辑,我们使用了一个系统道具阵列,它允许我们操纵实体(游戏对象),检测触摸,以及许多其他令人敬畏的细节,帮助我们制作一个简单的、功能性的游戏。
让我们在React Native中建立一个蛇形游戏
要制作一个游戏,我们需要一个画布或容器,我们将在其中添加游戏对象。要制作一个画布,我们只需添加一个带有风格的视图组件,像这样。
// 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,以便日后使用。
我们还在项目的根部创建了一个 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的网格,我们的蛇将在那里移动。
这时我们的游戏引擎已经设置好了,以显示蛇和它的食物。我们需要将实体和道具添加到 GameEngine ,但在此之前,我们需要创建一个蛇和食物的组件,在设备上渲染。
创建游戏实体
让我们首先制作蛇。蛇分为两部分,头部和身体(或尾巴)。现在我们将制作蛇的头部,我们将在本教程的后面添加蛇的尾巴。
为了制作蛇的头部,我们将在组件文件夹中制作一个 Head 组件。
正如你所看到的,我们有三个组件: Head , Food ,和 Tail 。我们将在本教程中逐一查看这些文件的内容。
在 Head 组件中,我们将创建一个带有一些样式的视图。
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" 属性来轻松移动头部。
这将呈现一个正方形,我们不打算使用更复杂的东西;一个正方形或长方形的形状代表蛇的身体,一个圆形的形状代表食物。
现在让我们将这条蛇的头部添加到 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 组件类似,但我们改变了背景颜色和边框半径,使其成为一个圆形。
现在创建一个 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 组件,这将表明它必须变大。
我们将循环浏览所有的元素来创建整个蛇身,附加上它,然后渲染。
在制作完所有需要的组件后,让我们把这两个组件作为 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 />,
},
}}
/>
为了保证食物位置的随机性,我们做了一个带有最小和最大参数的 randomPositions 函数。
在 tail ,我们在初始状态下添加了一个空数组,所以当蛇吃到食物时,它将在 elements: 空间中存储每个尾巴的长度。
在这一点上,我们已经成功创建了我们的游戏组件。现在是在游戏循环中添加游戏逻辑的时候了。
游戏逻辑
为了使游戏循环, GameEngine 组件有一个叫 systems 的道具,它接受一个数组的函数。
为了保持一切结构化,我正在创建一个名为 systems 的文件夹,并插入一个名为 GameLoop.js 的文件。
在这个文件中,我们正在导出一个带有某些参数的函数。
// GameLoop.js
export default function (entities, { events, dispatch }) {
...
return entities;
}
第一个参数是 entities ,它包含了我们传递给 GameEngine 组件的所有实体,所以我们可以操作它们。另一个参数是一个带有属性的对象,即 events 和 dispatch 。
移动蛇头
让我们编写代码,将蛇头向正确的方向移动。
在 GameLoop.js 函数中,我们将更新头部的位置,因为这个函数在每一帧都会被调用。
// GameLoop.js
export default function (entities, { events, dispatch }) {
const head = entities.head;
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
}
我们使用 entities 参数访问头部,在每一帧中我们都要更新蛇头的位置。
如果你现在玩游戏,什么也不会发生,因为我们把 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 值被更新回初始值,从而移动蛇的头部。
现在,蛇的速度应该比以前慢了。
"游戏结束!"条件
在这一点上,我们还没有添加 "游戏结束!"条件。第一个 "游戏结束!"条件是当蛇碰到墙时,游戏停止运行,并向用户显示一条信息,表明游戏已经结束。
为了添加这个条件,我们使用这段代码。
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 函数来发送一个 "game-over" 事件。
通过 else ,我们正在更新蛇的头部位置。
现在让我们添加 "游戏结束!"的功能。
每当我们派发一个 "game-over" 事件时,我们将停止游戏,并显示一个警告:"游戏结束!"让我们来实现它。
为了监听 "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 中,我们已经添加了 systems 道具,并通过我们的 GameLoop 函数传入了一个数组,同时还有一个 running 道具和一个 isGameRunning 状态。最后,我们添加了 onEvent 道具,它接受一个带有事件参数的函数,这样我们就可以监听我们的事件。
在这种情况下,我们在switch语句中监听 "game-over" 事件,所以当我们收到该事件时,我们显示 "Game over!" 警报,并将 isGameRunning 状态设置为 false ,以停止游戏。
食用食物
我们已经写好了 "游戏结束!"的逻辑,现在让我们来写一下让蛇吃食物的逻辑。
当蛇吃了食物后,食物的位置应该随机变化。
打开 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 ,以检查蛇头和食物的位置是否相同(这将表明蛇已经 "吃 "了食物)。然后,我们使用 randomPositions 函数更新食物的位置,正如我们在上面的 App.js 。请注意,我们是通过 entities 参数来访问食物的。
控制蛇
现在让我们来添加蛇的控制。我们将使用按钮来控制蛇的移动位置。
要做到这一点,我们需要在画布下面的屏幕上添加按钮。
// 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,
},
});
除了控制之外,我们还添加了一个按钮,以便在前一个游戏结束时开始一个新的游戏。这个按钮只在游戏没有运行时出现。在点击该按钮时,我们通过使用游戏引擎的 swap 函数来重置游戏,传入实体的初始对象,并更新游戏的运行状态。
现在说说控制。我们已经添加了可触摸物体,当按下这些物体时,就会派发将在游戏循环中处理的事件。
// 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 语句来识别事件并更新蛇的方向。
还在听我说吗?很好!唯一剩下的就是尾巴了。
尾巴功能
当蛇吃了食物后,我们希望它的尾巴能长出来。我们还想在蛇咬到自己的尾巴或身体时发出一个 "游戏结束!"的事件。
让我们来添加尾巴逻辑。
// 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),
];
}
}
为了使尾巴跟随蛇的头部,我们要更新尾巴的元素。我们通过将头部的位置添加到元素数组的开头,然后删除尾巴元素数组上的最后一个元素来实现这一目的。
在这之后,我们写一个条件,如果蛇咬了自己的身体,我们就分派 "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;
}
结语
现在你的第一个React Native游戏已经完成了你可以在自己的设备上运行这个游戏来玩。我希望你能学到一些新的东西,也希望你能与你的朋友分享。
谢谢你的阅读,祝你有个愉快的一天。
The post How to build a simple game in React Native appeared first onLogRocket Blog .
以上就是用React Native构建一个简单的游戏的详细内容,更多关于React Native游戏的资料请关注编程网其它相关文章!