[譯] 使用 React, Redux, and SVG 開發遊戲 - 第 3 部分

帥若浮誇發表於2018-05-09

使用 React, Redux, and SVG 開發遊戲 - 第 3 部分

提示: 在這個系列中,你將學習如何使用 React 和 Redux 控制一堆 SVG 元素來建立一個遊戲。這個系列所需要的知識同樣也可以使你建立使用 React 和 Redux 的其他型別的動畫,而不只是遊戲。你能夠在下面的 GitHub 倉庫中找到文章中開發的最終程式碼:Aliens Go Home - 第 3 部分


React 遊戲:Aliens, Go Home!

在這個教程中你開發的遊戲叫做 Aliens, Go Home! 這個遊戲的想法很簡單,你有一門大炮,你將必須殺掉嘗試入侵地球的飛行物體。要殺掉這些飛行的物體,你將必須標示和點選 SVG canvas 來使你的大炮發射。

如果你有些疑惑,你可以發現完成了的遊戲並在這裡執行它。但是不要玩的太多,你還有工作必須做。

“我正在用 React,Redux 和 SVG元素

建立一個遊戲。”

之前,在第一部分和第二部分

這個系列的第一部分,你已經使用 create-react-app 來啟動你的 React 應用,你已經安裝和配置了 Redux 來管理遊戲的狀態。之後,在建立遊戲的元素時,例如 SkyGroundCannonBaseCannonPipe, 你已經學習瞭如何在 React 元件中使用 SVG。最終,你通過使用事件監聽方法給你的大炮新增動畫效果和一個 JavaScript interval 來觸發 Redux 的 action 更新 CannonBase 的角度。

這些為你提供了理解如何使用React,Redux和SVG來建立你的遊戲(和其他動畫)的方法。

第二部分,你已經建立了遊戲中其他的必須元素(例如 HeartFlyingObjectCannonBall),使你的玩家能夠開始遊戲,並使用 CSS 動畫讓飛行物體飛起來(這就是他們應該做的事,對麼?)。

就算是我們有了這些非常好的特性,但是他們還沒有構成一個完整的遊戲。你仍然需要使你的大炮發射炮彈,並完成一個演算法來檢測飛行物體和炮彈的碰撞。除此之外,你必須在你的玩家殺死外星人的時候,增加 CurrentScore

殺死外星人和看到當前分數的增長很酷,但是你可能會使這個遊戲更有吸引力。。這就是為什麼你要在你的遊戲中增加一個排行榜特性。這將會使你的玩家花費更多的時間來達到排行榜的高位。

有了這些特性,你可以說你有了一個完整的遊戲。所以,為了節約時間,是時候關注他們了。

提示: 如果(無論是什麼原因)你沒有 前面兩部分 建立的程式碼,你可以從 這個 GitHub 倉庫 克隆他們。克隆之後,你能夠繼續跟隨接下來板塊中的指示。

在你的 React 遊戲裡實現排行榜特性

第一件你要做的使你的遊戲看起來更像一個真正的遊戲的事情就是實現排行榜特性。這個特性將使玩家能夠登陸,所以你的遊戲能夠跟蹤他們的最高分數和他們的排名。

整合 React 和 Auth0

要使 Auth0 管理你的玩家的身份,你必須有一個 Auth0 賬戶。如果你還沒有,你可以 在這裡 註冊一個免費 Auth0 賬戶

註冊完你的賬戶之後,你只需要建立一個 Auth0 應用 來代表你的遊戲。要做這個,前往 Auth0 的儀表盤中的 Application 頁面 ,然後點選 Create Application 按鈕。儀表盤將會給你展示一個表單,你必須輸入你的應用的 nametype 。你能輸入 Aliens, Go Home! 作為名字,並選擇 Single Page Web Application 作為型別(畢竟你的遊戲是基於 React 的 SPA)。然後,你可以點選 Create

建立 Auth0 應用來代表你的遊戲。

當你點選這個按鈕,儀表盤將會把你重定向到你的新應用的 Quick Start 標籤頁。正如你將在這篇文章中學習如何整合 React 和 Auth0,你不需要使用這個標籤頁。取而代之的,你將需要使用 Settings 標籤頁,所以我們前往這個頁面。

這裡有三件事你需要在這個標籤頁做。第一件是新增 http://localhost:3000 到名為 Allowed Callback URLs 的欄位。正如儀表盤解釋的, 在你的玩家認證之後, Auth0 只會回跳到這個欄位 URLs 中的一個 。所以,如果你想在網路上釋出你的遊戲,不要忘了在那裡同樣加入你的外網 URL (例如 http://aliens-go-home.digituz.com.br)。

在這個欄位輸入你所有的 URLs 之後,點選 Save 按鈕或者按下 ctrl + s (如果你是用的是 MacBook,你需要按下 command + s)。

你需要做的最後兩件事是複製 DomainClient ID 欄位的值。不管怎樣,在你使用這些值之前,你需要敲一些程式碼。

對於初學者,你將需要在你遊戲的根目錄下輸入以下命令來安裝 auth0-web 包:

npm i auth0-web
複製程式碼

正如你將看到的,這個包將有助於整合 Auth0 和 SPAs。

下一步是在你的遊戲中增加一個登陸按鈕,使你的玩家能夠通過 Auth0\ 認證。完成這個,要在 ./src/components 目錄下建立一個名為 Login.jsx 的檔案,加入以下的程式碼:

import React from 'react';
import PropTypes from 'prop-types';

const Login = (props) => {
  const button = {
    x: -300, // half width
    y: -600, // minus means up (above 0)
    width: 600,
    height: 300,
    style: {
      fill: 'transparent',
      cursor: 'pointer',
    },
    onClick: props.authenticate,
  };

  const text = {
    textAnchor: 'middle', // center
    x: 0, // center relative to X axis
    y: -440, // 440 up
    style: {
      fontFamily: '"Joti One", cursive',
      fontSize: 45,
      fill: '#e3e3e3',
      cursor: 'pointer',
    },
    onClick: props.authenticate,
  };

  return (
    <g filter="url(#shadow)">
      <rect {...button} />
      <text {...text}>
        Login to participate!
      </text>
    </g>
  );
};

Login.propTypes = {
  authenticate: PropTypes.func.isRequired,
};

export default Login;
複製程式碼

你剛剛建立的元件當被點選的時候會做什麼是不可知的。你需要在把它加入 Canvas 元件的時候定義它的操作。所以,開啟 Canvas.jsx 檔案,參照下面更新它:

// ... other import statements
import Login from './Login';
import { signIn } from 'auth0-web';

const Canvas = (props) => {
  // ... const definitions
  return (
    <svg ...>
      // ... other elements

      { ! props.gameState.started &&
      <g>
        // ... StartGame and Title components
        <Login authenticate={signIn} />
      </g>
      }

      // ... flyingObjects.map
    </svg>
  );
};
// ... propTypes definition and export statement
複製程式碼

正如你看見的,在這個新版本里,你已經引入了 Login 元件和 auth0-web 包裡的 signIn 方法。然後,你已經把你的新元件加入到了程式碼塊中,只在玩家沒有開始遊戲的時候出現。同樣的,你已經預料到,當點選的時候,登陸按鈕一定會觸發 signIn 方法。

當這些變化發生的時候,最後一件你必須做的事是在你的 Auth0 應用的屬性中配置 auth0-web。要做這件事,需要開啟 App.js 檔案並按照下面更新它:

// ... other import statements
import * as Auth0 from 'auth0-web';

Auth0.configure({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientID: 'YOUR_AUTH0_CLIENT_ID',
  redirectUri: 'http://localhost:3000/',
  responseType: 'token id_token',
  scope: 'openid profile manage:points',
});

class App extends Component {
  // ... constructor definition

  componentDidMount() {
    const self = this;

    Auth0.handleAuthCallback();

    Auth0.subscribe((auth) => {
      console.log(auth);
    });

    // ... setInterval and onresize
  }

  // ... trackMouse and render functions
}

// ... propTypes definition and export statement
複製程式碼

提示: 你必須使用從你的 Auth0 應用中複製的 DomainClient ID 欄位的值來替換YOUR_AUTH0_DOMAINYOUR_AUTH0_CLIENT_ID。除此之外,當你在網路上釋出你的遊戲的時候,你同樣需要替換 redirectUri 的值。

這個檔案裡的增強的點十分簡單。這個列表總結了他們:

  1. configure:你使用這個函式,協同你的 Auth0 應用的屬性,來配置 auth0-web 包。
  2. handleAuthCallback:你在 componentDidMount 生命週期的鉤子函式 觸發這個方法,來檢測使用者是否是經過 Auth0 認證的。 這個方法只是嘗試從 URL 抓取 tokens,並且如果成功,抓取使用者的文件並把所有的資訊儲存到localstorage
  3. subscribe:你使用這個方法來來記錄玩家是否是經過認證的(true認證過,false 代表其他)。

就是這樣,你的遊戲已經 使用 Auth0 作為它的身份管理服務。如果你現在啟動你的應用(npm start)並且在你的瀏覽器中瀏覽 (http://localhost:3000),你講看到登陸按鈕。點選它,它會把你重定向到 Auth0 登陸頁面,在這裡你可以登陸。

當你完成了流程中的註冊,Ahth0 會再一次把你重定向到你的遊戲,handleAuthCallback 方法將會抓去你的 tokens。然後,正如你已經告訴你的應用 console.log 所有的認證狀態的變化,你將能夠看到它在你的瀏覽器控制檯列印了 true

在你的 React 和 Redux 遊戲中展示登陸按鈕

“使用 Auth0 來保護你的遊戲是簡單和痛苦小的。”

建立排行榜 React 元件

現在你已經配置了 Auth0 作為你的身份管理系統,你將需要建立展示排行榜和當前玩家最大分數的元件。為這個,你將建立兩個元件:LeaderboardRank。你將需要將這個特性拆分成兩個元件,因為正如你所看到的,友好的展示玩家的資料(比如最大分數,姓名,位置和圖片)並不是簡單的事。其實也並不困難,但是你需要編寫一些好的程式碼。所以,把所有的東西加到一個元件之中會看起來很笨拙。

正如你的遊戲還沒有任何玩家,第一件事你需要做的就是定義一些 mock 資料來填充排行榜。做這件事最好的地方就是在 Canvas 元件中。同樣,因為你正要去更新你的 canvas,你能夠繼續深入,使用 Leaderboard 替換 Login 元件(你一會兒將在 Leaderboard 中加入 Login):

// ... other import statements
// replace Login with the following line
import Leaderboard from './Leaderboard';

const Canvas = (props) => {
  // ... const definitions
  const leaderboard = [
    { id: 'd4', maxScore: 82, name: 'Ado Kukic', picture: 'https://twitter.com/KukicAdo/profile_image', },
    { id: 'a1', maxScore: 235, name: 'Bruno Krebs', picture: 'https://twitter.com/brunoskrebs/profile_image', },
    { id: 'c3', maxScore: 99, name: 'Diego Poza', picture: 'https://twitter.com/diegopoza/profile_image', },
    { id: 'b2', maxScore: 129, name: 'Jeana Tahnk', picture: 'https://twitter.com/jeanatahnk/profile_image', },
    { id: 'e5', maxScore: 34, name: 'Jenny Obrien', picture: 'https://twitter.com/jenny_obrien/profile_image', },
    { id: 'f6', maxScore: 153, name: 'Kim Maida', picture: 'https://twitter.com/KimMaida/profile_image', },
    { id: 'g7', maxScore: 55, name: 'Luke Oliff', picture: 'https://twitter.com/mroliff/profile_image', },
    { id: 'h8', maxScore: 146, name: 'Sebastián Peyrott', picture: 'https://twitter.com/speyrott/profile_image', },
  ];
  return (
    <svg ...>
      // ... other elements

      { ! props.gameState.started &&
      <g>
        // ... StartGame and Title
        <Leaderboard currentPlayer={leaderboard[6]} authenticate={signIn} leaderboard={leaderboard} />
      </g>
      }

      // ... flyingObjects.map
    </svg>
  );
};

// ... propTypes definition and export statement
複製程式碼

在這個檔案的新版本中,你定義一個儲存假玩家的叫做 leaderboard 的陣列常量。這些玩家有以下屬性:idmaxScorenamepicture。然後,在 svg 元素中,你增加具有以下引數的 Leaderboard 元件:

  • currentPlayer: 這個定義了當前玩家的身份。現在,你正在使用之前定義的假玩家中的一個,所以你能夠看到每一件事是怎麼工作的。傳遞這個引數的目的是使你的排行榜高亮當前玩家。
  • authenticate: 這個和你加入到之前版本的 Login 元件中的引數是一樣的。
  • leaderboard: 這個是家玩家的陣列列表。你的排行榜將會使用這個來展示當前的排行。

現在,你必須定義 Leaderboard 元件。要做這個,需要在 ./src/components 目錄下建立一個名為 Leaderboard.jsx 的新檔案,並且加入如下程式碼:

import React from 'react';
import PropTypes from 'prop-types';
import Login from './Login';
import Rank from "./Rank";

const Leaderboard = (props) => {
  const style = {
    fill: 'transparent',
    stroke: 'black',
    strokeDasharray: '15',
  };

  const leaderboardTitle = {
    fontFamily: '"Joti One", cursive',
    fontSize: 50,
    fill: '#88da85',
    cursor: 'default',
  };

  let leaderboard = props.leaderboard || [];
  leaderboard = leaderboard.sort((prev, next) => {
    if (prev.maxScore === next.maxScore) {
      return prev.name <= next.name ? 1 : -1;
    }
    return prev.maxScore < next.maxScore ? 1 : -1;
  }).map((member, index) => ({
    ...member,
    rank: index + 1,
    currentPlayer: member.id === props.currentPlayer.id,
  })).filter((member, index) => {
    if (index < 3 || member.id === props.currentPlayer.id) return member;
    return null;
  });

  return (
    <g>
      <text filter="url(#shadow)" style={leaderboardTitle} x="-150" y="-630">Leaderboard</text>
      <rect style={style} x="-350" y="-600" width="700" height="330" />
      {
        props.currentPlayer && leaderboard.map((player, idx) => {
          const position = {
            x: -100,
            y: -530 + (70 * idx)
          };
          return <Rank key={player.id} player={player} position={position}/>
        })
      }
      {
        ! props.currentPlayer && <Login authenticate={props.authenticate} />
      }
    </g>
  );
};

Leaderboard.propTypes = {
  currentPlayer: PropTypes.shape({
    id: PropTypes.string.isRequired,
    maxScore: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    picture: PropTypes.string.isRequired,
  }),
  authenticate: PropTypes.func.isRequired,
  leaderboard: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.string.isRequired,
    maxScore: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    picture: PropTypes.string.isRequired,
    ranking: PropTypes.number,
  })),
};

Leaderboard.defaultProps = {
  currentPlayer: null,
  leaderboard: null,
};

export default Leaderboard;
複製程式碼

不要害怕!這個元件的程式碼非常簡單:

  1. 你定義常量 leaderboardTitle 來設定你的排行榜標題是什麼樣的。
  2. 你定義常量 dashedRectangle 來設定作為你的排行榜容器的 rect 元素的樣式。
  3. 你呼叫 props.leaderboard 變數的 sort 方法來排序。之後,你的排行榜就會使最高分在上面,最低分在下面。同樣,如果有兩個玩家打平手,你根據姓名將他們排序。
  4. 你在上一步(sort 方法)的結果上呼叫 map 方法,使用他們的 rank 和 具有 currentPlayer 的標誌來補充玩家資訊。你將使用這個標誌來高亮當前玩家出現的行。
  5. 你在上一步(map 方法)的結果上呼叫 filter 方法來刪除每一個不在前三名玩家的人。事實上,如果當前玩家不屬於這個篩選組,你要使當前玩家保留在最終的陣列裡。
  6. 最後,如果有一個使用者登陸(props.currentPlayer && leaderboard.map)或者正在展示 Login 按鈕,你遍歷過濾過得陣列來展示 Rank 元素。

最後一件你需要做的事就是建立 Rank React component。要完成這個,建立一個名為 Rank.jsx 新檔案,同時包括具有以下程式碼的 Leaderboard.jsx 檔案:

import React from 'react';
import PropTypes from 'prop-types';

const Rank = (props) => {
  const { x, y } = props.position;

  const rectId = 'rect' + props.player.rank;
  const clipId = 'clip' + props.player.rank;

  const pictureStyle = {
    height: 60,
    width: 60,
  };

  const textStyle = {
    fontFamily: '"Joti One", cursive',
    fontSize: 35,
    fill: '#e3e3e3',
    cursor: 'default',
  };

  if (props.player.currentPlayer) textStyle.fill = '#e9ea64';

  const pictureProperties = {
    style: pictureStyle,
    x: x - 140,
    y: y - 40,
    href: props.player.picture,
    clipPath: `url(#${clipId})`,
  };

  const frameProperties = {
    width: 55,
    height: 55,
    rx: 30,
    x: pictureProperties.x,
    y: pictureProperties.y,
  };

  return (
    <g>
      <defs>
        <rect id={rectId} {...frameProperties} />
        <clipPath id={clipId}>
          <use xlinkHref={'#' + rectId} />
        </clipPath>
      </defs>
      <use xlinkHref={'#' + rectId} strokeWidth="2" stroke="black" />
      <text filter="url(#shadow)" style={textStyle} x={x - 200} y={y}>{props.player.rank}º</text>
      <image {...pictureProperties} />
      <text filter="url(#shadow)" style={textStyle} x={x - 60} y={y}>{props.player.name}</text>
      <text filter="url(#shadow)" style={textStyle} x={x + 350} y={y}>{props.player.maxScore}</text>
    </g>
  );
};

Rank.propTypes = {
  player: PropTypes.shape({
    id: PropTypes.string.isRequired,
    maxScore: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    picture: PropTypes.string.isRequired,
    rank: PropTypes.number.isRequired,
    currentPlayer: PropTypes.bool.isRequired,
  }).isRequired,
  position: PropTypes.shape({
    x: PropTypes.number.isRequired,
    y: PropTypes.number.isRequired
  }).isRequired,
};

export default Rank;
複製程式碼

這個程式碼同樣沒有什麼可怕的。唯一不平常的事就是你加入到這個元件的是 clipPath 元素 和一個在 defs 元素中的 rect 元素來建立一個圓的肖像。

有了這些新檔案,你能夠前往你的應用(http://localhost:3000/)來看看你的新排行榜特性。

在你的 React 遊戲中展示排行榜

使用 Socket.IO 開發一個實時排行榜

帥氣,你已經使用 Auth0 作為你的身份管理服務,並且你也建立了需要展示排行榜的元件。之後,你需要做什麼?對了,你需要一個能出發實時事件的後端來更新排行榜。

這可能使你想到:開發一個實時後端伺服器困難麼?不,不困難。使用 Socket.IO,你可以在很短的時間實現這個特性。不管怎樣,在深入之前,你可能想要好糊這個後端服務,對不對?要做這個,你需要建立一個 Auth0 API 來代表你的服務。

這樣做很簡單。前往 你的 Auth0 儀表盤的 APIs 頁面 並且點選 Create API 按鈕,Auth0 會想你展示一個有三個資訊需要填的表單:

  1. API的 Name :這裡,你僅僅需要宣告一個友好的名字使你不至於忘掉這個 API 代表的什麼。所以,在這個區域輸入 Aliens, Go Home! 就好啦。
  2. API的 Identifier :這裡建議的值是你遊戲的最終 URL,但是事實上這可以是任何東西,雖然這樣,在這裡輸入 https://aliens-go-home.digituz.com.br
  3. Signing Algorithm :這裡有兩個選項, RS256HS256 。你最好不要修改這個欄位(例如,保持 RS256)。你過你想要學習他們之間的不同,檢視 這個答案

為 Socket.IO 實時服務建立 Auth0 API

在你填完這個表單後,點選 Create 按鈕。會將你重定向到你的新 API 中叫做 Quick Start 的標籤頁。在那裡,點選 Scopes 標籤並且新增叫做 manage:points 的新作用域,他有以下的描述:“讀和寫最大的分數”。在 Auth0 APIs 上定義作用域是很好的實踐

新增完這個作用域之後,你能夠繼續程式設計。來完成你的實時排行榜服務,按照下面的做:

# 在專案根目錄建立一個服務目錄
mkdir server

# 進入服務目錄
cd server

# 作為一個 NPM 專案啟動它
npm init -y

# 安裝一些依賴
npm i express jsonwebtoken jwks-rsa socket.io socketio-jwt

# 建立一個儲存伺服器原始碼的檔案
touch index.js
複製程式碼

然後,在這個新檔案中,新增以下程式碼:

const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const client = jwksClient({
  jwksUri: 'https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json'
});

const players = [
  { id: 'a1', maxScore: 235, name: 'Bruno Krebs', picture: 'https://twitter.com/brunoskrebs/profile_image', },
  { id: 'c3', maxScore: 99, name: 'Diego Poza', picture: 'https://twitter.com/diegopoza/profile_image', },
  { id: 'b2', maxScore: 129, name: 'Jeana Tahnk', picture: 'https://twitter.com/jeanatahnk/profile_image', },
  { id: 'f6', maxScore: 153, name: 'Kim Maida', picture: 'https://twitter.com/KimMaida/profile_image', },
  { id: 'e5', maxScore: 55, name: 'Luke Oliff', picture: 'https://twitter.com/mroliff/profile_image', },
  { id: 'd4', maxScore: 146, name: 'Sebastián Peyrott', picture: 'https://twitter.com/speyrott/profile_image', },
];

const verifyPlayer = (token, cb) => {
  const uncheckedToken = jwt.decode(token, {complete: true});
  const kid = uncheckedToken.header.kid;

  client.getSigningKey(kid, (err, key) => {
    const signingKey = key.publicKey || key.rsaPublicKey;

    jwt.verify(token, signingKey, cb);
  });
};

const newMaxScoreHandler = (payload) => {
  let foundPlayer = false;
  players.forEach((player) => {
    if (player.id === payload.id) {
      foundPlayer = true;
      player.maxScore = Math.max(player.maxScore, payload.maxScore);
    }
  });

  if (!foundPlayer) {
    players.push(payload);
  }

  io.emit('players', players);
};

io.on('connection', (socket) => {
  const { token } = socket.handshake.query;

  verifyPlayer(token, (err) => {
    if (err) socket.disconnect();
    io.emit('players', players);
  });

  socket.on('new-max-score', newMaxScoreHandler);
});

http.listen(3001, () => {
  console.log('listening on port 3001');
});
複製程式碼

在學習這部分程式碼做什麼之前,使用你的 Auth0 域(和你新增到 App.js 檔案是一樣那個)替換 YOUR_AUTH0_DOMAIN。你可以在 jwksUri 屬性值中找到這個佔位符。

現在,為了理解這個事情是怎麼工作的,檢視這個列表:

  • expresssocket.io:這只是一個通過 Socket.IO 加強的 Express 伺服器來使它具備實時的特性。如果你以前沒有用過 Socket.IO,檢視他們的 Get Started 教程。它真的很簡單。
  • jwtjwksClient:當 Auth0 認證的時候,你的玩家(在其他事情之外)會在 JWT (JSON Web Token) 表單中得到一個 access_token。因為你使用 RS256 簽名演算法,你需要使用 jwksClient 包來獲取正確的公鑰來認證 JWTs。你收到的 JWTs 中包含一個 kid 屬性(Key ID),你可以使用這個屬性得到正確的公鑰(如果你感到困惑,你可以在這兒瞭解更多地 JWKS)。
  • jwt.verify:在找到正確的鑰匙之後,你可以使用這個方法來解碼和認證 JWTs。如果他們都很好,你就給請求的人傳送 players 列表。如果他們沒有經過認證,你 disconnect 這個 socket(使用者)。
  • on('new-max-score', ...):最後,你在 new-max-score 事件上附加 newMaxScoreHandler 方法。因此,無論什麼時候你需要更新一個使用者的最高分,你會需要在你的 React 應用中觸發這個事件。

剩餘的程式碼非常直觀。因此,你能關注在你的遊戲中整合這個服務。

Socket.IO 和 React

在建立你的實時後端服務之後,是時候將它整合到你的 React 遊戲中了。使用 React 和 Socket.IO 最好的方式是安裝 socket.io-client。你可以在你的 React 應用根目錄下輸入以下命令來安裝它:

npm i socket.io-client
複製程式碼

然後,在那之後,無論什麼時候玩家認證,你將使你的遊戲連線你的服務(你不需要給沒有認證的玩家顯示排行榜)。因為你使用 Redux 來儲存遊戲的狀態,你需要兩個 actions 來保持你的 Redux 儲存最新。因此,開啟 ./src/actions/index.js 檔案並且按照下面來更新它:

export const LEADERBOARD_LOADED = 'LEADERBOARD_LOADED';
export const LOGGED_IN = 'LOGGED_IN';
// ... MOVE_OBJECTS and START_GAME ...

export const leaderboardLoaded = players => ({
  type: LEADERBOARD_LOADED,
  players,
});

export const loggedIn = player => ({
  type: LOGGED_IN,
  player,
});

// ... moveObjects and startGame ...
複製程式碼

這個新版本定義在兩種情況下會被觸發的 actions:

  1. LOGGED_IN:當一個玩家登陸,你使用這個 action 連線你的 React 遊戲到實時服務。
  2. LEADERBOARD_LOADED:當實時服務傳送玩家列表,你使用這個 action 用這些玩家來更新 Redux 儲存。

要使你的 Redux 儲存迴應這些 actions,開啟 ./src/reducers/index.js 檔案並且按照下面來更新它:

import {
  LEADERBOARD_LOADED, LOGGED_IN,
  MOVE_OBJECTS, START_GAME
} from '../actions';
// ... other import statements

const initialGameState = {
  // ... other game state properties
  currentPlayer: null,
  players: null,
};

// ... initialState definition

function reducer(state = initialState, action) {
  switch (action.type) {
    case LEADERBOARD_LOADED:
      return {
        ...state,
        players: action.players,
      };
    case LOGGED_IN:
      return {
        ...state,
        currentPlayer: action.player,
      };
    // ... MOVE_OBJECTS, START_GAME, and default cases
  }
}

export default reducer;
複製程式碼

現在,無論你的遊戲什麼時候觸發 LEADERBOARD_LOADED action,你會使用新的玩家陣列列表來更新你的 Redux 儲存。除此之外,無論什麼時候一個玩家登陸(LOGGED_IN),你將在你的儲存中更新 currentPlayer

然後,為了是你的遊戲使用這些新的 actions, 開啟 ./src/containers/Game.js 檔案並且按照下面來更新它:

// ... other import statements
import {
  leaderboardLoaded, loggedIn,
  moveObjects, startGame
} from '../actions/index';

const mapStateToProps = state => ({
  // ... angle and gameState
  currentPlayer: state.currentPlayer,
  players: state.players,
});

const mapDispatchToProps = dispatch => ({
  leaderboardLoaded: (players) => {
    dispatch(leaderboardLoaded(players));
  },
  loggedIn: (player) => {
    dispatch(loggedIn(player));
  },
  // ... moveObjects and startGame
});

// ... connect and export statement
複製程式碼

有了它,你準備好了使你的遊戲接入實時服務來載入和更新排行榜。因此,開啟 ./src/App.js 檔案並且按照下面來更新它:

// ... other import statements
import io from 'socket.io-client';

Auth0.configure({ 
  // ... other properties
  audience: 'https://aliens-go-home.digituz.com.br',
});

class App extends Component {
  // ... constructor

  componentDidMount() {
    const self = this;

    Auth0.handleAuthCallback();

    Auth0.subscribe((auth) => {
      if (!auth) return;

      const playerProfile = Auth0.getProfile();
      const currentPlayer = {
        id: playerProfile.sub,
        maxScore: 0,
        name: playerProfile.name,
        picture: playerProfile.picture,
      };

      this.props.loggedIn(currentPlayer);

      const socket = io('http://localhost:3001', {
        query: `token=${Auth0.getAccessToken()}`,
      });

      let emitted = false;
      socket.on('players', (players) => {
        this.props.leaderboardLoaded(players);

        if (emitted) return;
        socket.emit('new-max-score', {
          id: playerProfile.sub,
          maxScore: 120,
          name: playerProfile.name,
          picture: playerProfile.picture,
        });
        emitted = true;
        setTimeout(() => {
          socket.emit('new-max-score', {
            id: playerProfile.sub,
            maxScore: 222,
            name: playerProfile.name,
            picture: playerProfile.picture,
          });
        }, 5000);
      });
    });

    // ... setInterval and onresize
  }

  // ... trackMouse

  render() {
    return (
      <Canvas
        angle={this.props.angle}
        currentPlayer={this.props.currentPlayer}
        gameState={this.props.gameState}
        players={this.props.players}
        startGame={this.props.startGame}
        trackMouse={event => (this.trackMouse(event))}
      />
    );
  }
}

App.propTypes = {
  // ... other propTypes definitions
  currentPlayer: PropTypes.shape({
    id: PropTypes.string.isRequired,
    maxScore: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    picture: PropTypes.string.isRequired,
  }),
  leaderboardLoaded: PropTypes.func.isRequired,
  loggedIn: PropTypes.func.isRequired,
  players: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.string.isRequired,
    maxScore: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    picture: PropTypes.string.isRequired,
  })),
};

App.defaultProps = {
  currentPlayer: null,
  players: null,
};

export default App;
複製程式碼

正如你在上面看到的程式碼,你做了這些:

  1. 配置了 Auth0 模組上的 audience 屬性;
  2. 抓去了當前玩家的個人資料(Auth0.getProfile())來建立 currentPlayer 常量,並且更新了 Redux 儲存(this.props.loggedIn(...));
  3. 用玩家的 access_token 連線你的實時服務(io('http://localhost:3001', ...));
  4. 監聽實時服務觸發的玩家事件,更新 Redux 儲存(this.props.leaderboardLoaded(...));

然後,你的遊戲還沒有完成,你的玩家還不能殺死外星人,你加入一些臨時程式碼模擬 new-max-score 事件。第一,你出發一個新的 120 分的 maxScore,把登陸的玩家放在第五的位置。然後,五秒鐘(setTimeout(..., 5000))之後,你出發一個新的 222 分的 maxScore,把登陸的玩家放在第二的位置。

除了這些變化,你向你的 Canvas 傳入兩個新的屬性: currentPlayerplayers。因此,你需要開啟 ./src/components/Canvas.jsx 並且更新它:

// ... import statements

const Canvas = (props) => {
  // ... gameHeight and viewBox constants

  // REMOVE the leaderboard constant !!!!

  return (
    <svg ...>
      // ... other elements

      { ! props.gameState.started &&
      <g>
        // ... StartGame and Title
        <Leaderboard currentPlayer={props.currentPlayer} authenticate={signIn} leaderboard={props.players} />
      </g>
      }

      // ... flyingObjects.map
    </svg>
  );
};

Canvas.propTypes = {
  // ... other propTypes definitions
  currentPlayer: PropTypes.shape({
    id: PropTypes.string.isRequired,
    maxScore: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    picture: PropTypes.string.isRequired,
  }),
  players: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.string.isRequired,
    maxScore: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    picture: PropTypes.string.isRequired,
  })),
};

Canvas.defaultProps = {
  currentPlayer: null,
  players: null,
};

export default Canvas;
複製程式碼

在這個檔案裡,你需要做以下的變更:

  1. 刪除常量 leaderboard。現在,你通過你的實時服務載入這個常量。
  2. 更新 <Leaderboard /> 元素。你現在已經有了更多地真是資料了:props.currentPlayer and props.players
  3. 加強 propTypes 的定義使 Canvas 元件能夠使用 currentPlayerplayers 的值。

好了!你已經整合了你的 React 遊戲排行榜和 Socket.IO 實時服務。要測試所有的事務,執行以下的命令:

# 進入實時服務的目錄
cd server

# 在後臺執行這個命令
node index.js &

# 回到你的遊戲
cd ..

# 啟動 React 開發服務
npm start
複製程式碼

然後,在瀏覽器中開啟你的遊戲(http://localhost:3000)。這樣,在登陸之後,你就能看到你出現在了第五的位置,5秒鐘之後,你就會跳到第二的位置。

測試你的 React 遊戲的 Socket.IO 實時排行榜

實現剩餘的部分

現在,你已經差不多完成了你的遊戲的所有東西。你已經建立了遊戲需要的 React 元素,你已經新增了絕大部分的動畫效果,你已經實現了排行榜特性。這個難題的遺失的部分是:

  • Shooting Cannon Balls :為了殺外星人,你必須允許你的玩家射擊大炮炮彈。
  • Detecting Collisions :正像你的遊戲會有大炮炮彈,飛行的物體到到處動,你必須實現一個檢測這些物體碰撞的演算法。
  • Updating Lives and the Current Score :在實現你的玩家殺死飛行物體之後,你的遊戲必須增加他們當前的分數,以至於他們能夠達到新的最大分數。同樣的,你需要在飛行物體入侵地球之後減掉生命。
  • Updating the Leaderboard :當實現了上面的所有特性,最後一件你需要做的事是用新的最高分數更新排行榜。

所以,在接下來的部分,你將關注實現這些部分來完成你的遊戲。

發射大炮炮彈

要使你的玩家射擊大炮炮彈,你將在你的 Canvas 新增一個 onClick 時間偵聽器。然後,當點選的時候,你的 canvas 會觸發 Redux 的 action 新增一個炮彈到 Redux store(實際上就是你的遊戲的 state)。炮彈的移動將被 moveObjects reducer 處理。

要開始實現這個特性,你可以從建立 Redux action 開始。要做這個,開啟 ./src/actions/index.js 檔案,加入以下程式碼:

// ... other string constants

export const SHOOT = 'SHOOT';

// ... other function constants

export const shoot = (mousePosition) => ({
  type: SHOOT,
  mousePosition,
});
複製程式碼

然後,你能夠準備 reducer(./src/reducers/index.js)來處理這個 action:

import {
  LEADERBOARD_LOADED, LOGGED_IN,
  MOVE_OBJECTS, SHOOT, START_GAME
} from '../actions';
// ... other import statements
import shoot from './shoot';

const initialGameState = {
  // ... other properties
  cannonBalls: [],
};

// ... initialState definition

function reducer(state = initialState, action) {
  switch (action.type) {
    // other case statements
    case SHOOT:
      return shoot(state, action);
    // ... default statement
  }
}
複製程式碼

正如你看到的,你的 reducer 的新版本在接收到 SHOOT action 時,使用 shoot 方法。你仍然需要定義這個方法。所以,在和 reducer 同樣的目錄下建立一個名為 shoot.js 的檔案,並加入以下程式碼:

import { calculateAngle } from '../utils/formulas';

function shoot(state, action) {
  if (!state.gameState.started) return state;

  const { cannonBalls } = state.gameState;

  if (cannonBalls.length === 2) return state;

  const { x, y } = action.mousePosition;

  const angle = calculateAngle(0, 0, x, y);

  const id = (new Date()).getTime();
  const cannonBall = {
    position: { x: 0, y: 0 },
    angle,
    id,
  };

  return {
    ...state,
    gameState: {
      ...state.gameState,
      cannonBalls: [...cannonBalls, cannonBall],
    }
  };
}

export default shoot;
複製程式碼

這個方法從檢查這個遊戲是否啟動為開始。如果沒有啟動,它只是返回當前的狀態。否則,它會檢查遊戲中是否已經有兩個炮彈。你通過限制炮彈的數量來使遊戲變得更困難一點。如果玩家發射了少於兩發的炮彈,這個函式使用 calculateAngle 定義新炮彈的彈道。然後,最後,這個函式建立了一個新的代表炮彈的物件並且返回了一個新的 Redux store 的 state。

在定義這個 action 和 reducer 處理它之後,你將更新 Game 容器給 App 元件提供 action。所以,開啟 ./src/containers/Game.js 檔案並且按照下面的來更新它:

// ... other import statements
import {
  leaderboardLoaded, loggedIn,
  moveObjects, startGame, shoot
} from '../actions/index';

// ... mapStateToProps

const mapDispatchToProps = dispatch => ({
  // ... other functions
  shoot: (mousePosition) => {
    dispatch(shoot(mousePosition))
  },
});

// ... connect and export
複製程式碼

現在,你需要更新 ./src/App.js 檔案來使用你的 dispatch wrapper:

// ... import statements and Auth0.configure

class App extends Component {
  constructor(props) {
    super(props);
    this.shoot = this.shoot.bind(this);
  }

  // ... componentDidMount and trackMouse definition

  shoot() {
    this.props.shoot(this.canvasMousePosition);
  }

  render() {
    return (
      <Canvas
        // other props
        shoot={this.shoot}
      />
    );
  }
}

App.propTypes = {
  // ... other propTypes
  shoot: PropTypes.func.isRequired,
};

// ... defaultProps and export statements
複製程式碼

正如你在這裡看到的,你在 App 的類中定義一個新的方法使用 canvasMousePosition 來呼叫 shoot dispatcher。然後,你傳遞把這個新的方法傳遞到 Canvas 元件。所以,你仍然需要加強這個元件,將這個方法附加到 svg 元素的 onClick 事件監聽器並且使它渲染加農炮彈:

// ... other import statements
import CannonBall from './CannonBall';

const Canvas = (props) => {
  // ... gameHeight and viewBox constant

  return (
    <svg
      // ... other properties
      onClick={props.shoot}
    >
      // ... defs, Sky and Ground elements

      {props.gameState.cannonBalls.map(cannonBall => (
        <CannonBall
          key={cannonBall.id}
          position={cannonBall.position}
        />
      ))}

      // ... CannonPipe, CannonBase, CurrentScore, etc
    </svg>
  );
};

Canvas.propTypes = {
  // ... other props
  shoot: PropTypes.func.isRequired,
};

// ... defaultProps and export statement
複製程式碼

提示:CannonPipe 之前 新增 cannonBalls.map 很重要,否則炮彈將和大炮自身重疊。

這些改變足夠是你的遊戲在炮彈的初始位置新增炮彈了(x: 0, y: 0)並且 他們的彈道(angle)已經定義好。現在的問題是這些物件是沒有動畫的(其實就是他們不會動)。

要使他們動,你將需要在 ./src/utils/formulas.js 檔案中新增兩個函式:

// ... other functions

const degreesToRadian = degrees => ((degrees * Math.PI) / 180);

export const calculateNextPosition = (x, y, angle, divisor = 300) => {
  const realAngle = (angle * -1) + 90;
  const stepsX = radiansToDegrees(Math.cos(degreesToRadian(realAngle))) / divisor;
  const stepsY = radiansToDegrees(Math.sin(degreesToRadian(realAngle))) / divisor;
  return {
    x: x +stepsX,
    y: y - stepsY,
  }
};
複製程式碼

提示: 要學習上面工作的的公式,看這裡

你將在新的名為 moveCannonBalls.js 的檔案中使用 calculateNextPosition 方法。所以,在 ./src/reducers/ 目錄中建立這個檔案,並加入以下程式碼:

import { calculateNextPosition } from '../utils/formulas';

const moveBalls = cannonBalls => (
  cannonBalls
    .filter(cannonBall => (
      cannonBall.position.y > -800 && cannonBall.position.x > -500 && cannonBall.position.x < 500
    ))
    .map((cannonBall) => {
      const { x, y } = cannonBall.position;
      const { angle } = cannonBall;
      return {
        ...cannonBall,
        position: calculateNextPosition(x, y, angle, 5),
      };
    })
);

export default moveBalls;
複製程式碼

在這個檔案暴露的方法中,你做了兩件重要的事情。第一,你使用 filter 方法去除了沒有再特定區域中的 cannonBalls。這就是,你刪除了 Y-axis 座標小於 -800,或者向左邊移動太多的(小於 -500),或者向右邊移動太多的(大於 500)。

最後,要使用這個方法,你將需要將 ./src/reducers/moveObjects.js 按照下面來重構:

// ... other import statements
import moveBalls from './moveCannonBalls';

function moveObjects(state, action) {
  if (!state.gameState.started) return state;

  let cannonBalls = moveBalls(state.gameState.cannonBalls);

  // ... mousePosition, createFlyingObjects, filter, etc

  return {
    ...newState,
    gameState: {
      ...newState.gameState,
      flyingObjects,
      cannonBalls,
    },
    angle,
  };
}

export default moveObjects;
複製程式碼

在這個檔案的新版本中,你簡單的加強了之前的 moveObjects reducer 來使用新的 moveBalls 函式。然後,你使用這個函式的結果來給 gameStatecannonBalls 屬性定義一個新陣列。

現在,完成了這些更改之後,你的玩家能夠發射炮彈了。你可以在一個瀏覽器中通過測試你的遊戲來檢視這一點。

在一個使用 React,Redux 和 SVGs 的遊戲中使玩家能夠發射炮彈

檢測碰撞

現在你的遊戲支援發射炮彈並且這裡有飛行的物體入侵地球,這是一個好的時機新增一個檢測碰撞的演算法。有了這個演算法,你可以刪除相碰撞的炮彈和飛行物體。這也使你能夠繼續接下來的特性: 增加當前的分數

一個好的實現這個檢測碰撞演算法的策略是把炮彈和飛行物體想象成為矩形。儘管這個策略不如按照物體真實形狀實現的演算法準確,但是把它們作為矩形處理會使每件事情變得簡單。除此之外,對於這個遊戲,你不需要很精確,因為,幸運的是,你不需要這個演算法殺死真的外星人。

在腦袋中有這個想法之後,新增接下來的方法到 ./src/utils/formulas.js 檔案中:

// ... other functions

export const checkCollision = (rectA, rectB) => (
  rectA.x1 < rectB.x2 && rectA.x2 > rectB.x1 &&
  rectA.y1 < rectB.y2 && rectA.y2 > rectB.y1
);
複製程式碼

正像你看到的,把這些物件按照矩形來看待,使你在這些簡單的情況下檢測是否重疊。現在,為了使用這個函式,在 ./src/reducers 目錄下,建立一個名為 checkCollisions.js 的新檔案,新增以下的程式碼:

import { checkCollision } from '../utils/formulas';
import { gameHeight } from '../utils/constants';

const checkCollisions = (cannonBalls, flyingDiscs) => {
  const objectsDestroyed = [];
  flyingDiscs.forEach((flyingDisc) => {
    const currentLifeTime = (new Date()).getTime() - flyingDisc.createdAt;
    const calculatedPosition = {
      x: flyingDisc.position.x,
      y: flyingDisc.position.y + ((currentLifeTime / 4000) * gameHeight),
    };
    const rectA = {
      x1: calculatedPosition.x - 40,
      y1: calculatedPosition.y - 10,
      x2: calculatedPosition.x + 40,
      y2: calculatedPosition.y + 10,
    };
    cannonBalls.forEach((cannonBall) => {
      const rectB = {
        x1: cannonBall.position.x - 8,
        y1: cannonBall.position.y - 8,
        x2: cannonBall.position.x + 8,
        y2: cannonBall.position.y + 8,
      };
      if (checkCollision(rectA, rectB)) {
        objectsDestroyed.push({
          cannonBallId: cannonBall.id,
          flyingDiscId: flyingDisc.id,
        });
      }
    });
  });
  return objectsDestroyed;
};

export default checkCollisions;
複製程式碼

檔案中的這些程式碼基本上做了下面幾件事:

  1. 定義了一個名為 objectsDestroyed 的陣列來儲存所有毀掉的東西。
  2. 通過迭代 flyingDiscs 陣列(使用 forEach 方法)建立矩形來代表飛行物。提示,因為你使用 CSS 動畫來使物體移動,你需要基於 currentLifeTime 的 Y-axis 計算他們位置。
  3. 通過迭代 cannonBalls 陣列(使用 forEach 方法)建立矩形來代表炮彈。
  4. 呼叫 checkCollision 方法,來決定這兩個矩形是否必須被摧毀。然後,如果他們必須被摧毀,他們被新增到 objectsDestroyed 陣列,由這個方法返回。

Lastly, you will need to update the moveObjects.js file to use this function as follows: 最後,你需要更新 moveObjects.js 檔案,參照下面來使用這個方法:

// ... import statements

import checkCollisions from './checkCollisions';

function moveObjects(state, action) {
  // ... other statements and definitions

  // the only change in the following three lines is that it cannot
  // be a const anymore, it must be defined with let
  let flyingObjects = newState.gameState.flyingObjects.filter(object => (
    (now - object.createdAt) < 4000
  ));

  // ... { x, y } constants and angle constant

  const objectsDestroyed = checkCollisions(cannonBalls, flyingObjects);
  const cannonBallsDestroyed = objectsDestroyed.map(object => (object.cannonBallId));
  const flyingDiscsDestroyed = objectsDestroyed.map(object => (object.flyingDiscId));

  cannonBalls = cannonBalls.filter(cannonBall => (cannonBallsDestroyed.indexOf(cannonBall.id)));
  flyingObjects = flyingObjects.filter(flyingDisc => (flyingDiscsDestroyed.indexOf(flyingDisc.id)));

  return {
    ...newState,
    gameState: {
      ...newState.gameState,
      flyingObjects,
      cannonBalls,
    },
    angle,
  };
}

export default moveObjects;
複製程式碼

這裡,你使用 checkCollisions 函式的結果從 cannonBallsflyingObjects 陣列中移除物件。

現在,當炮彈和飛行物體重疊,新版本的 moveObjects reducer 把它們從 gameState 刪除。你可以在瀏覽器中看到這個 action。

更新生命數和當前分數

無論什麼時候飛行的物體入侵地球,你必須減少玩家持有的命的數量。所以,當玩家沒有更多地生命值的時候,你必須結束遊戲。要實現這些特性,你只需要更新兩個檔案。第一個檔案是 ./src/reducers/moveObject.js。你需要按照下面來更新它:

import { calculateAngle } from '../utils/formulas';
import createFlyingObjects from './createFlyingObjects';
import moveBalls from './moveCannonBalls';
import checkCollisions from './checkCollisions';

function moveObjects(state, action) {
  // ... code until newState.gameState.flyingObjects.filter

  const lostLife = state.gameState.flyingObjects.length > flyingObjects.length;
  let lives = state.gameState.lives;
  if (lostLife) {
    lives--;
  }

  const started = lives > 0;
  if (!started) {
    flyingObjects = [];
    cannonBalls = [];
    lives = 3;
  }

  // ... x, y, angle, objectsDestroyed, etc ...

  return {
    ...newState,
    gameState: {
      ...newState.gameState,
      flyingObjects,
      cannonBalls: [...cannonBalls],
      lives,
      started,
    },
    angle,
  };
}

export default moveObjects;
複製程式碼

這些行新程式碼只是簡單的比較了 flyingObjects 陣列和其在 state 中的初始長度來決定玩家是否失去生命。這個策略有效是因為你把這些程式碼新增在了彈出飛行物體之後並且在刪除碰撞物體之前。這些飛行物體在遊戲中保持 4 秒鐘((now - object.createdAt) < 4000)。所以,如果這些陣列的長度發生了變化,就意味著飛行物體入侵了地球。

現在,給玩家展示他們的生命數,你需要更新 Canvas 元件。所以,開啟 ./src/components/Canvas.jsx 檔案並且按照下面來更新:

// ... other import statements
import Heart from './Heart';

const Canvas = (props) => {
  // ... gameHeight and viewBox constants

  const lives = [];
  for (let i = 0; i < props.gameState.lives; i++) {
    const heartPosition = {
      x: -180 - (i * 70),
      y: 35
    };
    lives.push(<Heart key={i} position={heartPosition}/>);
  }

  return (
    <svg ...>
      // ... all other elements

      {lives}
    </svg>
  );
};

// ... propTypes, defaultProps, and export statements
複製程式碼

有了這些更改,你的遊戲幾乎完成了。玩家已經能夠發射和殺死飛行物體,並且如果太多的它們進攻地球,遊戲結束。現在,為了完成這部分,你需要更新玩家當前的分數,這樣他們才能比較誰殺了更多地外星人。

做這個來加強你的遊戲很簡單。你只需要按以下來更新 ./src/reducers/moveObjects.js 這個檔案:

// ... import statements

function moveObjects(state, action) {
  // ... everything else

  const kills = state.gameState.kills + flyingDiscsDestroyed.length;

  return {
    // ...newState,
    gameState: {
      // ... other props
      kills,
    },
    // ... angle,
  };
}

export default moveObjects;
複製程式碼

然後,在 ./src/components.Canvas.jsx 檔案,你需要用這個來替換 CurrentScore 元件(硬編碼值為 15):

<CurrentScore score={props.gameState.kills} />
複製程式碼

“我使用 React、Redux、SVG 和 CSS 動畫建立一個遊戲。”

更新排行榜

好訊息!更新排行榜是你說你使用 React、Redux、SVG 和 CSS 動畫完成了一個遊戲所需要做的最後一件事。同樣的,正如你看到的,這裡的工作很快並且沒有痛苦。

第一,你需要更新 ./server/index.js 檔案來重置 players 陣列。你不希望你釋出的遊戲裡是假使用者和假結果。所以,開啟這個檔案並且刪除所有的假玩家/結果。最後,你會有像下面這樣定義的常量:

const players = [];
複製程式碼

然後,你需要重構 App 元件。所以,開啟 ./src/App.js 檔案並且做下面的修改:

// ... import statetments

// ... Auth0.configure

class App extends Component {
  constructor(props) {
    // ... super and this.shoot.bind(this)
    this.socket = null;
    this.currentPlayer = null;
  }

  // replace the whole content of the componentDidMount method
  componentDidMount() {
    const self = this;

    Auth0.handleAuthCallback();

    Auth0.subscribe((auth) => {
      if (!auth) return;

      self.playerProfile = Auth0.getProfile();
      self.currentPlayer = {
        id: self.playerProfile.sub,
        maxScore: 0,
        name: self.playerProfile.name,
        picture: self.playerProfile.picture,
      };

      this.props.loggedIn(self.currentPlayer);

      self.socket = io('http://localhost:3001', {
        query: `token=${Auth0.getAccessToken()}`,
      });

      self.socket.on('players', (players) => {
        this.props.leaderboardLoaded(players);
        players.forEach((player) => {
          if (player.id === self.currentPlayer.id) {
            self.currentPlayer.maxScore = player.maxScore;
          }
        });
      });
    });

    setInterval(() => {
      self.props.moveObjects(self.canvasMousePosition);
    }, 10);

    window.onresize = () => {
      const cnv = document.getElementById('aliens-go-home-canvas');
      cnv.style.width = `${window.innerWidth}px`;
      cnv.style.height = `${window.innerHeight}px`;
    };
    window.onresize();
  }

  componentWillReceiveProps(nextProps) {
    if (!nextProps.gameState.started && this.props.gameState.started) {
      if (this.currentPlayer.maxScore < this.props.gameState.kills) {
        this.socket.emit('new-max-score', {
          ...this.currentPlayer,
          maxScore: this.props.gameState.kills,
        });
      }
    }
  }

  // ... trackMouse, shoot, and render method
}

// ... propTypes, defaultProps, and export statement
複製程式碼

做一個總結,這些是你在這個元件中做的更改:

  • 你在它的類裡面定義兩個新屬性(socketcurrentPlayer),這樣你就能在不同的方法裡使用它們。
  • 你刪除用來觸發模擬 new-max-score 事件的假的最高分。
  • 你通過迭代 players 陣列(你從 Socket.IO 後臺接收到的)來設定玩家正確的最高分。就這樣,如果他們再一次回來啊,他們仍然會有 maxScore 記錄
  • 你定義 componentWillReceiveProps 生命週期來檢查玩家是否打到了一個新的 maxScore。如果是,你的遊戲觸發一個 new-max-score 事件去更新排行榜

這就是了!你的遊戲已經準備好了第一次。要看所有的行為,用下面的程式碼執行 Socket.IO 後臺和你的 React 應用:

# 在後臺執行後端服務
node ./server/index &

# 執行 React 應用
npm start
複製程式碼

然後,執行瀏覽器,使用不同得 email 地址認證,並且殺一些外星人。你可以看到,當遊戲結束的時候,排行榜將會在兩個瀏覽器更新。

Aliens, Go Home! 遊戲完成。

總結

在這個系列中,你使用了很多驚人的技術來建立一個好遊戲。你使用了 React 來定義和控制遊戲元素,你使用了 SVG(代替 HTML)來渲染這些元素,你使用了 Redux 來控制遊戲的狀態,並且你使用了 CSS 動畫使外星人在螢幕上運動。哦,除此之外,你甚至使用了一點 Socket.IO 使你的排行榜是實時的,並使用 Auth0 作為你遊戲的身份管理系統。

唉!你走了很長的路,你在這三篇文章中學了很多。可能是時候休息一下,玩會兒你的遊戲了。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章