您当前的位置:首页 >> 创意
手把手教你玩eos:卡牌游戏第八课——优化细节体验
发布时间:2019-06-09
 

文章由EOS节点EOSWing(柚翼)首发于eoswing.io博客,并向知币(ZHIBI.COM)授权刊载。内容将不定期更新,仅供学习交流之用。

0.引言

0.1教程概况

手把手教你玩eos系列教程,从最基础开始,一步一步教你学会用eos。比如发代币,开发DAPP等等。
本文是第十四篇。本篇教程是卡牌游戏的最后一课,讲解怎么进一步优化整个游戏的细节体验。


0.2 学习内容

  1. 相关准备

  2. 编写前端代码

  3. 测试代码


0.3 机器环境

  • cpu: 1核

  • 内存: 8G

  • 操作系统:CentOS 7.4 64位

  • 服务器所在地:香港


推荐将服务器放在网络较为优质的环境,比如香港。不然会有很多配置依赖下载上的问题。


1 相关准备

1.1 课程目标

在本课程中,我们将优化游戏细节体验,比如添加游戏帮助对话框,进度提示,优化加载体验等等。


1.2 准备工作

进入开发环境容器

docker exec -it eosdev /bin/bash

进入前端文件夹

cd /eos-work/frontend/


2 编写前端代码

2.1 添加游戏帮助对话框

在GameInfo中添加RulesModal模态对话框。


cd ./src/components/Game/components/GameInfo


svn checkout https://github.com/EOSIO/eosio-card-game-repo/branches/lesson-8/frontend/src/components/Game/components/GameInfo/components

在GameInfo.jsx中加入RulesModal组件


vi GameInfo.jsx

更新代码如下:


import React, { Component } from 'react';

// Components

import { Button } from 'components';


//在此次添加引用

// Game subcomponents

import { RulesModal } from './components';


class Info extends Component {

  render() {

    // Extract data and event functions from props

    const { className, deckCardCount, handCardCount, onEndGame } = this.props;

    // Display:

    // Round number: 18 <-- ((max deck = 17) + 1) - Deck Cards - Hand Cards

    // Rules button to trigger a modal

    // Button to end the current game

    return (

      <div className={`Info${ className ? ' ' + className : '' }`}>

        { <p>ROUND <span className="round-number">{ 18 - deckCardCount - handCardCount }/17</span></p> }

//在此处添加组件

        <RulesModal />

        <div><Button onClick={ onEndGame } className="small red">QUIT</Button></div>

      </div>

    )

  }

}


export default Info;


2.2 添加进度提示


cd /eos-work/frontend/src/components/Button/


vi Button.jsx

编辑代码如下:


import React, { Component } from 'react';


class Button extends Component {


  constructor(props) {

    // Inherit constructor

    super(props);

    // Component state setup

    this.state = {

      loading: false,

    };

    // Bind functions

    this.handleClick = this.handleClick.bind(this);

  }


  handleClick() {

    const { onClick } = this.props;

    // Show the loading indicator in case the action to be performed takes too long

    this.setState({ loading: true });


    // If the prop onClick is a function, invoke it and stores its return value in ``promise``

    // If the prop onClick is NOT a function, the value of ``promise`` will be false

    const promise = typeof onClick === "function" && onClick();


    // If ``promise`` is a function (a Promise), invoke setState after it has been resolved.

    if (promise && typeof promise.then === "function") {

      return promise.then(() => {

        this.isComponentMounted && this.setState({ loading: false });

      });

    }

    // Otherwise, just invoke setState directly

    this.isComponentMounted && this.setState({ loading: false });

  }


  componentDidMount() {

    this.isComponentMounted = true;

  }


  componentWillUnmount() {

    this.isComponentMounted = false;

  }


  render() {

    const { className, type, style, children } = this.props;

    let { loading } = this.state;

    // Enable the loading CSS class if either the private state attribute `loading`

    // or the props `loading` is true

    loading = loading || this.props.loading;

    return (

      <button

        className={`Button${ className ? ' ' + className : '' }${ loading ? ' loading' : '' }`}

        onClick={ this.handleClick }

        { ...{ type, style } }

      >{ children }</button>

    );

  }


}


export default Button;

在Game.jsx中更新引用


cd /eos-work/frontend/src/components/Game/


vi Game.jsx

编辑代码如下:


// React core

import React, { Component } from 'react';

import { connect } from 'react-redux';

// Game subcomponents

import { GameInfo, GameMat, PlayerProfile, Resolution } from './components';

// Services and redux action

import { UserAction } from 'actions';

import { ApiService } from 'services';


class Game extends Component {


  constructor(props) {

    // Inherit constructor

    super(props);

    // State for showing/hiding components when the API (blockchain) request is loading

    this.state = {

      loading: true,

    };

    // Bind functions

    this.loadUser = this.loadUser.bind(this);

    this.handleStartGame = this.handleStartGame.bind(this);

    this.handlePlayCard = this.handlePlayCard.bind(this);

    this.handleNextRound = this.handleNextRound.bind(this);

    this.handleEndGame = this.handleEndGame.bind(this);

    // Call `loadUser` before mounting the app

    this.loadUser();

  }


  // Get latest user object from blockchain

  loadUser() {

    // Extract `setUser` of `UserAction` and `user.name` of UserReducer from redux

    const { setUser, user: { name } } = this.props;

    // Send request the blockchain by calling the ApiService,

    // Get the user object and store the `win_count`, `lost_count` and `game_data` object

    return ApiService.getUserByName(name).then(user => {

      setUser({

        win_count: user.win_count,

        lost_count: user.lost_count,

        game: user.game_data,

      });

      // Set the loading state to false for displaying the app

      this.setState({ loading: false });

    });

  }


  handleStartGame() {

    // Send a request to API (blockchain) to start game

    // And call `loadUser` again for react to render latest game status to UI

    return ApiService.startGame().then(()=>{

      return this.loadUser();

    });

  }


  handlePlayCard(cardIdx) {

    // Extract `user.game` of `UserReducer` from redux

    const { user: { game } } = this.props;

    // If it is an empty card, not going to do anything

    if (game.hand_player[cardIdx] === 0) {

      return;

    }

    // Show the loading indicator if the connection took too long

    this.setState({ loading: true });

    // Send a request to API (blockchain) to play card with card index

    // And call `loadUser` again for react to render latest game status to UI

    return ApiService.playCard(cardIdx).then(()=>{

      return this.loadUser();

    });

  }


  handleNextRound() {

    // Send a request to API (blockchain) to trigger next round

    // And call `loadUser` again for react to render latest game status to UI

    return ApiService.nextRound().then(()=>{

      return this.loadUser();

    });

  }


  handleEndGame() {

    // Send a request to API (blockchain) to end the game

    // And call `loadUser` again for react to render latest game status to UI

    return ApiService.endGame().then(()=>{

      return this.loadUser();

    });

  }


  render() {

    // Extract data from state and user data of `UserReducer` from redux

    const { loading } = this.state;

    const { user: { name, win_count, lost_count, game } } = this.props;


    // Flag to indicate if the game has started or not

    // By checking if the deckCard of AI is still 17 (max card)

    const isGameStarted = game && game.deck_ai.length !== 17;


    // If game hasn't started, display `PlayerProfile`

    // If game has started, display `GameMat`, `Resolution`, `Info` screen

    return (

      <section className={`Game${ (loading ? " loading" : "") }`}>

        { !isGameStarted ?

            <PlayerProfile

              name={ name }

              winCount={ win_count }

              lostCount={ lost_count }

              onLogout={ this.handleLogout }

              onStartGame={ this.handleStartGame }

            />

          :

            <div className="container">

              <GameMat

                deckCardCount={ game.deck_ai.length }

                aiLife={ game.life_ai }

                aiHandCards={ game.hand_ai }

                aiName="COMPUTER"

                playerLife={ game.life_player }

                playerHandCards={ game.hand_player }

                playerName={ name }

                onPlayCard={ this.handlePlayCard }

              />

              <Resolution

                status={ game.status }

                aiCard={ game.selected_card_ai }

                aiName="COMPUTER"

                aiLost={ game.life_lost_ai }

                playerCard={ game.selected_card_player }

                playerName={ name }

                playerLost={ game.life_lost_player }

                onNextRound={ this.handleNextRound }

                onEndGame={ this.handleEndGame }

              />

              <GameInfo

                deckCardCount={ game.deck_ai.length }

                handCardCount={ game.hand_ai.filter( x => x > 0 ).length }

                onEndGame={ this.handleEndGame }

              />

            </div>

        }

        {

          isGameStarted && loading &&

          <div className="spinner">

            <div className="image"></div>

            <div className="circles">

              <div className="circle">

                <div className="inner"></div>

              </div>

              <div className="circle">

                <div className="inner"></div>

              </div>

              <div className="circle">

                <div className="inner"></div>

              </div>

              <div className="circle">

                <div className="inner"></div>

              </div>

              <div className="circle">

                <div className="inner"></div>

              </div>

            </div>

          </div>

        }

      </section>

    )

  }


}


// Map all state to component props (for redux to connect)

const mapStateToProps = state => state;


// Map the following action to props

const mapDispatchToProps = {

  setUser: UserAction.setUser,

};


// Export a redux connected component

export default connect(mapStateToProps, mapDispatchToProps)(Game);


2.3 优化加载体验

现在每次刷新游戏屏幕时都会短暂显示登录屏幕。


这是由ApiService从区块链获取最新用户数据引起的。


在检索用户数据时,dApp认为用户没有登录。


为了防止这种情况,我们将向App.jsx添加一个名为loading的组件状态。


在检索区块链数据时出现,直到有反馈,确定是应用是在登录页面,游戏结束屏幕还是个人资料屏幕等等。


cd /eos-work/frontend/src/components/Login/


vi Login.jsx 

更新代码如下:


import React, { Component } from 'react';

import { connect } from 'react-redux';

// Components

import { Button } from 'components';

// Services and redux action

import { UserAction } from 'actions';

import { ApiService } from 'services';


class Login extends Component {


  constructor(props) {

    // Inherit constructor

    super(props);

    // State for form data and error message

    this.state = {

      form: {

        username: '',

        key: '',

        error: '',

      },

      isSigningIn: false,

    }

    // Bind functions

    this.handleChange = this.handleChange.bind(this);

    this.handleSubmit = this.handleSubmit.bind(this);

  }


  // Runs on every keystroke to update the React state

  handleChange(event) {

    const { name, value } = event.target;

    const { form } = this.state;


    this.setState({

      form: {

        ...form,

        [name]: value,

        error: '',

      },

    });

  }


  componentDidMount() {

    this.isComponentMounted = true;

  }


  componentWillUnmount() {

    this.isComponentMounted = false;

  }


  // Handle form submission to call api

  handleSubmit(event) {

    // Stop the default form submit browser behaviour

    event.preventDefault();

    // Extract `form` state

    const { form } = this.state;

    // Extract `setUser` of `UserAction` and `user.name` of UserReducer from redux

    const { setUser } = this.props;

    // Set loading spinner to the button

    this.setState({ isSigningIn: true });

    // Send a login transaction to the blockchain by calling the ApiService,

    // If it successes, save the username to redux store

    // Otherwise, save the error state for displaying the message

    return ApiService.login(form)

      .then(() => {

        setUser({ name: form.username });

      })

      .catch(err => {

        this.setState({ error: err.toString() });

      })

      .finally(() => {

        if (this.isComponentMounted) {

          this.setState({ isSigningIn: false });

        }

      });

  }


  render() {

    // Extract data from state

    const { form, error, isSigningIn } = this.state;


    return (

      <div className="Login">

        <div className="title">Elemental Battles - powered by EOSIO</div>

        <div className="description">Please use the Account Name and Private Key generated in the previous page to log into the game.</div>

        <form name="form" onSubmit={ this.handleSubmit }>

          <div className="field">

            <label>Account name</label>

            <input

              type="text"

              name="username"

              value={ form.username }

              placeholder="All small letters, a-z, 1-5 or dot, max 12 characters"

              onChange={ this.handleChange }

              pattern="[\.a-z1-5]{2,12}"

              required

            />

          </div>

          <div className="field">

            <label>Private key</label>

            <input

              type="password"

              name="key"

              value={ form.key }

              onChange={ this.handleChange }

              pattern="^.{51,}$"

              required

            />

          </div>

          <div className="field form-error">

            { error && <span className="error">{ error }</span> }

          </div>

          <div className="bottom">

            <Button type="submit" className="green" loading={ isSigningIn }>

              { "CONFIRM" }

            </Button>

          </div>

        </form>

      </div>

    )

  }

}


// Map all state to component props (for redux to connect)

const mapStateToProps = state => state;


// Map the following action to props

const mapDispatchToProps = {

  setUser: UserAction.setUser,

};


// Export a redux connected component

export default connect(mapStateToProps, mapDispatchToProps)(Login);

再在App.jsx中更新引用


cd /eos-work/frontend/src/components/App/


vi App.jsx

更新代码如下:


// React core

import React, { Component } from 'react';

import { connect } from 'react-redux';

// Components

import { Game, Login } from 'components';

// Services and redux action

import { UserAction } from 'actions';

import { ApiService } from 'services';


class App extends Component {


  constructor(props) {

    // Inherit constructor

    super(props);


    // 此处添加状态,默认loading为true

    // State for showing/hiding components when the API (blockchain) request is loading

    this.state = {

      loading: true,

    };

    // Bind functions

    this.getCurrentUser = this.getCurrentUser.bind(this);

    // Call getCurrentUser before mounting the app

    this.getCurrentUser();

  }


  getCurrentUser() {

    // Extract setUser of UserAction from redux

    const { setUser } = this.props;

    // Send a request to API (blockchain) to get the current logged in user

    return ApiService.getCurrentUser()

      // If the server return a username

      .then(username => {

        // Save the username to redux store

        // For structure, ref: ./frontend/src/reducers/UserReducer.js

        setUser({ name: username });

      })

      // To ignore 401 console error

      .catch(() => {})

  //在获取到区块链数据反馈后,设置loading状态为false

      // Run the following function no matter the server return success or error

      .finally(() => {

        // Set the loading state to false for displaying the app

        this.setState({ loading: false });

      });

  }


  render() {

    // Extract data from state and props (`user` is from redux)

    const { loading } = this.state;

    const { user: { name, game } } = this.props;


// 确定游戏处于什么状态

    // Determine the app status for styling

    let appStatus = "login";

    if (game && game.status !== 0) {

      appStatus = "game-ended";

    } else if (game && game.selected_card_ai > 0) {

      appStatus = "card-selected";

    } else if (game && game.deck_ai.length !== 17) {

      appStatus = "started";

    } else if (name) {

      appStatus = "profile";

    }


    // Set class according to loading state, it will hide the app (ref to css file)

    // If the username is set in redux, display the Game component

    // If the username is NOT set in redux, display the Login component

    return (

      <div className={ `App status-${ appStatus }${ loading ? " loading" : "" }` }>

        { name && <Game /> }

        { !name && <Login /> }

      </div>

    );

  }


}


// Map all state to component props (for redux to connect)

const mapStateToProps = state => state;


// Map the following action to props

const mapDispatchToProps = {

  setUser: UserAction.setUser,

};


// Export a redux connected component

export default connect(mapStateToProps, mapDispatchToProps)(App);


3 测试代码


cd /eos-work/frontend


npm start

在浏览器中输入网址测试。

点击右边的rules按钮查看游戏规则帮助


选择卡牌后,在等待时会出现进度加载提示:


还有刷新后回到主界面等等,就不一一截图了。


至此,一个完整的EOS卡牌游戏就制作完成了。

4 后记

延伸阅读

在本文的学习中如果遇到问题,欢迎留言或者在如下链接寻找解决方案:

EOS官方游戏开发第四课: https://battles.eos.io/tutorial/lesson4/chapter1


如果觉得这一系列教程有点意思,请投票给柚翼节点(eoswingdotio)。您的投票是本教程持续更新的动力源泉,谢谢。


系列教程回顾:

手把手教你玩eos:教程开篇絮语及目录

手把手教你玩eos:(一)使用docker搭建eos本地运行环境

手把手教你玩eos:(二)钱包和账户的创建与管理

手把手教你玩eos:(三)使用智能合约创建和发放代币

手把手教你玩eos:(四)编写第一个智能合约Hello_eos

手把手教你玩eos:(五)编写智能合约游戏——三连棋

手把手教你玩eos:(六)架设EOS区块浏览器

手把手教你玩eos:(七)卡牌游戏第一课——搭建前后端框架

      手把手教你玩eos:(八)卡牌游戏第二课——存储状态和登录

      手把手教你玩eos:(九)卡牌游戏第三课——从区块链中读取状态

      手把手教你玩eos:(十)卡牌游戏第四课——游戏核心组件

      手把手教你玩eos:(十一)卡牌游戏第五课——AI部分

      手把手教你玩eos:(十二)卡牌游戏第六课——战斗部分

      手把手教你玩eos:(十三)卡牌游戏第七课——完善游戏流程


相关阅读