用React Native制作一個簡單的遊戲引擎

簡介

今天我們將學習如何使用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 是設置蛇頭大小的值。
  • xspeedyspeed 是決定蛇的運動和方向的值,可以是1、0或-1。註意,當 xspeed 被設置為1或-1時,那麼 yspeed 的值必須為0,反之亦然
  • 最後, renderer ,負責渲染該組件
  • updateFrequencynextMove 將在後面討論。

在添加完 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遊戲的資料請關註WalkonNet其它相關文章!

推薦閱讀: