1. BTC-比特币:¥26826(+0.33%)
  2. ETH-以太坊:¥913.75(-0.17%)
  3. EOS-柚子:¥24.54(+0.57%)
  4. GBI:4729.55
即将开始的项目:21 进行中的项目:52

手把手教你玩eos:卡牌游戏第八课——优化细节体验

来源:EOSWing博客

作者: EOSWing

发稿时间: 2019-01-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 后记

延伸阅读

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

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

系列教程回顾:

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

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

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

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

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

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

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

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

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

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

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

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

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

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