【React Native】Matching Game 配对游戏

基于React Native 的 跨三端(Android、iOS、Web)配对游戏

目录

  • 第一章 需求分析和功能

  • 第二章 框架架构和编码实现

第一章 需求分析和功能

我们知道现在有很多小游戏,但是这些小游戏要么只能Android运行,要么只能iOS运行,要么只能Web端运行。所以我想到了,要是能一次性编码解决跨端游戏开发就好了。

于是我就用React -Native来实现此次跨端小游戏的开发。

跨端小游戏的功能很简单,我主要实现游戏卡片的配对并计数。

刘海屏和全面屏底部的兼容

首先是头部的title,由于不能影响到有的刘海屏手机,所以我们要进行兼容考虑。

主页面中的图片反转业务逻辑

如果两次翻转的不一样,就会使两个图片还原,并统计次数。
如果全部翻转完了,进行关卡升级,增加图片个数。

在这里插入图片描述

第二章 框架架构和编码实现

框架架构

本项目是基于React-Native,所以首先需要进行React-Native环境配置(nodejs和react-native)
然后我们需要初始化一个React-Native项目

npx react-native init MatchingGame
  • App.js 进行项目主要数据、逻辑、组件、样式的引入和实现
  • Card.js 代表翻转的卡片组件,以备之后调用
    在这里插入图片描述
    之后我们在React-Native框架上进行开发就好啦

React-Native 简略讲解

React-Native是基于React.JS的移动开发框架,使用React.JS能够实现通过Javascript一次开发多端运行。

1. React-Native数据流基于MVVM、框架基于React

  • MVVM
    Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。
  • React
    React是Javascript的构建页面的框架

2. React实现方式

编程语言是JSX

JSX是综合HTML和JS的编程方式,也就是能够在JS语言中使用HTML,JSX在编译时会自动转成JS

React开发组件

父组件可以调用子组件
例如本项目就在header组件下面、footer上面也就是main组件中间插入Card卡片组件
也就是:

 <View style={ styles.main }>
	 <View style={ styles.gameBoard }>
	   {
	     this.state.cardSymbolsInRand.map((symbol, index) => {
	       return (
	         <Card
	           key={ index }
	           onPress={ () => this.cardPressHandler(index) }
	           style={ styles.button }
	           fontSize={ 30 }
	           title={ symbol }
	           cover="❓"
	           isShow={ this.state.isOpen[index] }
	         />
	       )
	     })
	   }
	 </View>
</View>

而这里Card组件创建如下

class Card extends Component {
  render () {
    return (
      <TouchableOpacity onPress={ this.props.onPress } style={{ ...this.props.style }}>
        {/* TouchableOpacity:点击时,有半透明的回馈 */}
        <Text
          style={{ fontSize: this.props.fontSize || 32 }}
        >{ this.props.isShow ? this.props.title : this.props.cover }</Text>
      </TouchableOpacity>
    )
  }
}

3.React数据流

类似事件捕获,数据流是自上而下单向地从父节点传递到子节点,所以组件是简单且容易把握的,他们只需要从父节点提供的props中获取数据并渲染即可。如果顶层组件的某个prop改变了,React会递归地向下遍历整棵组件数,重新渲染所有使用这个属性的组件。

改变组件样式的props属性

props类似于css的属性,它是组件里面的属性,能够从父组件中进行修改
例如card里面的Text组件里面就有之前我们引入的TouchableOpacity组件的属性

<TouchableOpacity onPress={ this.props.onPress } style={{ ...this.props.style }}>
  {/* TouchableOpacity:点击时,有半透明的回馈 */}
  <Text
    style={{ fontSize: this.props.fontSize || 32 }}
  >{ this.props.isShow ? this.props.title : this.props.cover }</Text>
</TouchableOpacity>
能够进行组件事件响应从而改变数据的state事件(view-model层)

例如本项目的图标点击事件
从view视图层绑定的监听事件触发过后能够传给view-model层,cardPressHandler函数就是这个view-model层,进行通过this.setState({ })进行数据修改。

// 每一次点击图标的事件
  cardPressHandler = (index) => {
    let isOpen = [...this.state.isOpen]

    // 如果点击的是已被翻开的图标,不执行后续代码
    if (isOpen[index]) {
      return
    }

    isOpen[index] = true

    if (this.state.firstPickedIndex == null && this.state.secondPickedIndex == null) {
      this.setState({
        isOpen,
        firstPickedIndex: index
      })
    } else if (this.state.firstPickedIndex != null && this.state.secondPickedIndex == null) {
      this.setState({
        isOpen,
        secondPickedIndex: index
      })
    }
    this.setState({
      steps: this.state.steps + 1
    })
  }
事件触发后进行DOM更新

在这里插入图片描述
这是一个React组件实现组件可交互所需的流程,

  1. render()输出虚拟DOM,
  2. 虚拟DOM转为DOM,
  3. 再在DOM上注册事件,
  4. 事件触发setState()修改数据,
  5. 在每次调用setState方法时,React会自动执行render方法来更新虚拟DOM,如果组件已经被渲染,那么还会更新到DOM中去。

在本项目中:我们首先render输出了集有各种组件的虚拟DOM,然后进行虚拟DOM到DOM的转换,然后绑定按钮监听事件cardPressHandler,事件触发后通过setState修改数据,然后进行更新

4.组件的生命周期

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 本项目涉及生命周期的地方有:
  • 初次载入界面渲染过后对卡片顺序进行打乱也就是通过componentDidMount ()进行卡片顺序打乱
initGame = () => {
  let newCardsSymbols = [...this.state.cardSymbols, ...this.state.cardSymbols]
  // 将 16 个 emoji 的顺序打乱
  let cardSymbolsInRand = this.shuffleArray(newCardsSymbols)
  // 存储打开状态
  let isOpen = []

  for (let i = 0; i < newCardsSymbols.length; i++) {
    isOpen.push(false)
  }
  
  this.setState({
    cardSymbolsInRand,
    isOpen
  })
}
componentDidMount () {
  this.initGame()
}
  • 在用户点击两个卡片,触发更新监听事件之后,需要对两个卡片进行统计,如果一样的话就会进行次数统计以及决定在屏幕上保不保留
componentDidUpdate (prevProps, prevState) {
  if (prevState.secondPickedIndex != this.state.secondPickedIndex) {
    this.calculateGameResult()
  }
}

编码实现

逻辑实现

1. 导入react-native库里面已经封装好的组件

状态栏 刘海屏适配 创建css属性 文字 点击事件

import React, { Component, Fragment } from 'react'
import {
  StatusBar,//状态栏
  SafeAreaView,//刘海屏适配
  View,
  Text,
  StyleSheet,//属性都写在这里
  Dimensions,//获取当前屏幕宽高
  TouchableOpacity//点击事件
} from 'react-native'
2. model层:state里面的数据定义

包括反转状态、图标顺序、反转次数、是否结束

state = {
    cardSymbols: [
      '❤', '🤩', '💩', '👍', '😂', '🥶', '😈', '😕'
    ],
    // 打乱顺序的 16 个图标
    cardSymbolsInRand: [],
    // 记录被图标反转的状态
    isOpen: [],
    // 第一次点击的图标
    firstPickedIndex: null,
    // 第二次点击的图标
    secondPickedIndex: null,
    // 记录所有图标被反转的次数
    steps: 0,
    // 判断游戏是否已结束
    isEnded: false
  }
3. 游戏初始化

打乱16个emoji表情顺序、将初始化状态传到state数据里

initGame = () => {
    let newCardsSymbols = [...this.state.cardSymbols, ...this.state.cardSymbols]
    // 将 16 个 emoji 的顺序打乱
    let cardSymbolsInRand = this.shuffleArray(newCardsSymbols)
    // 存储打开状态
    let isOpen = []

    for (let i = 0; i < newCardsSymbols.length; i++) {
      isOpen.push(false)
    }
    
    this.setState({
      cardSymbolsInRand,
      isOpen
    })
  }
  componentDidMount () {
    this.initGame()
  }
  shuffleArray = (arr) => {
    const newArr = arr.slice()
    for (let i = newArr.length - 1; i > 0; i--) {
      const rand = Math.floor(Math.random() * (i + 1));
      [newArr[i], newArr[rand]] = [newArr[rand], newArr[i]]
    }
    return newArr
  }
4. 绑定点击事件

进行了点击之前已翻开图片的冗余操作、如果翻开的两个的属性一样就进行state数据修改

// 每一次点击图标的事件
  cardPressHandler = (index) => {
    let isOpen = [...this.state.isOpen]

    // 如果点击的是已被翻开的图标,不执行后续代码
    if (isOpen[index]) {
      return
    }

    isOpen[index] = true

    if (this.state.firstPickedIndex == null && this.state.secondPickedIndex == null) {
      this.setState({
        isOpen,
        firstPickedIndex: index
      })
    } else if (this.state.firstPickedIndex != null && this.state.secondPickedIndex == null) {
      this.setState({
        isOpen,
        secondPickedIndex: index
      })
    }
    this.setState({
      steps: this.state.steps + 1
    })
  }
5. 每次渲染更新之后进行判断所有emoji表情是否都被打开

若打开那么就重置游戏

calculateGameResult = () => {
    if (this.state.firstPickedIndex != null && this.state.secondPickedIndex != null) {
      // 判断是否所有图标都被打开(结束游戏)
      if (this.state.cardSymbolsInRand.length > 0) {
        let totalOpens = this.state.isOpen.filter((isOpen) => isOpen)
        if (totalOpens.length === this.state.cardSymbolsInRand.length) {
          this.setState({
            isEnded: true
          })
          return
        }
      }

      let firstSymbol = this.state.cardSymbolsInRand[this.state.firstPickedIndex]
      let secondSymbol = this.state.cardSymbolsInRand[this.state.secondPickedIndex]

      if (firstSymbol != secondSymbol) {
        // Incorrect
        setTimeout(() => {
          let isOpen = [...this.state.isOpen]
          isOpen[this.state.firstPickedIndex] = false
          isOpen[this.state.secondPickedIndex] = false

          this.setState({
            firstPickedIndex: null,
            secondPickedIndex: null,
            isOpen
          })
        }, 1000)
      } else {
        // Correct
        this.setState({
          firstPickedIndex: null,
          secondPickedIndex: null
        })
      }
    }
  }

  componentDidUpdate (prevProps, prevState) {
    if (prevState.secondPickedIndex != this.state.secondPickedIndex) {
      this.calculateGameResult()
    }
  }
  resetGame = () => {
    this.initGame()

    this.setState({
      firstPickedIndex: null,
      secondPickedIndex: null,
      steps: 0,
      isEnded: false
    })
  }

视图实现

render () {
    return (
      <Fragment>
        <StatusBar />
        <SafeAreaView style={ styles.container }>
          <View style={ styles.header }>
            <Text style={ styles.heading }>Matching Game</Text>
          </View>
          <View style={ styles.main }>
            <View style={ styles.gameBoard }>
              {
                this.state.cardSymbolsInRand.map((symbol, index) => {
                  return (
                    <Card
                      key={ index }
                      onPress={ () => this.cardPressHandler(index) }
                      style={ styles.button }
                      fontSize={ 30 }
                      title={ symbol }
                      cover="❓"
                      isShow={ this.state.isOpen[index] }
                    />
                  )
                })
              }
            </View>
          </View>
          <View style={ styles.footer }>
            <Text style={ styles.footerText }>{
              this.state.isEnded
                ? `Congrats! You have completed in ${ this.state.steps } setps.`
                : `You have tried ${ this.state.steps } time(s).`
            }</Text>
            {
              this.state.isEnded ?
                <TouchableOpacity onPress={ this.resetGame } style={ styles.tryAgainButton }>
                  <Text style={ styles.tryAgainButtonText }>Try Again</Text>
                </TouchableOpacity>
                : null
            }
          </View>
        </SafeAreaView>
      </Fragment>
    )
  }
card卡片组件的复用

card组件绑定点击事件,如果点击就翻牌,两个不一样就会翻回去

import React, { Component } from 'react'
import {
  TouchableOpacity,
  Text
} from 'react-native'

class Card extends Component {
  render () {
    return (
      <TouchableOpacity onPress={ this.props.onPress } style={{ ...this.props.style }}>
        {/* TouchableOpacity:点击时,有半透明的回馈 */}
        <Text
          style={{ fontSize: this.props.fontSize || 32 }}
        >{ this.props.isShow ? this.props.title : this.props.cover }</Text>
      </TouchableOpacity>
    )
  }
}

export default Card

全部代码

import React, { Component, Fragment } from 'react'
import {
  StatusBar,
  SafeAreaView,
  View,
  Text,
  StyleSheet,
  Dimensions,
  TouchableOpacity
} from 'react-native'

import Card from './src/components/Card'

/* 
  制作配对游戏:
    1. 建构界面
    2. 组件与事件
    3. 游戏逻辑
*/

export default class App extends Component {
  state = {
    cardSymbols: [
      '❤', '🤩', '💩', '👍', '😂', '🥶', '😈', '😕'
    ],
    // 打乱顺序的 16 个图标
    cardSymbolsInRand: [],
    // 记录被图标反转的状态
    isOpen: [],
    // 第一次点击的图标
    firstPickedIndex: null,
    // 第二次点击的图标
    secondPickedIndex: null,
    // 记录所有图标被反转的次数
    steps: 0,
    // 判断游戏是否已结束
    isEnded: false
  }

  initGame = () => {
    let newCardsSymbols = [...this.state.cardSymbols, ...this.state.cardSymbols]
    // 将 16 个 emoji 的顺序打乱
    let cardSymbolsInRand = this.shuffleArray(newCardsSymbols)
    // 存储打开状态
    let isOpen = []

    for (let i = 0; i < newCardsSymbols.length; i++) {
      isOpen.push(false)
    }
    
    this.setState({
      cardSymbolsInRand,
      isOpen
    })
  }

  componentDidMount () {
    this.initGame()
  }

  shuffleArray = (arr) => {
    const newArr = arr.slice()
    for (let i = newArr.length - 1; i > 0; i--) {
      const rand = Math.floor(Math.random() * (i + 1));
      [newArr[i], newArr[rand]] = [newArr[rand], newArr[i]]
    }
    return newArr
  }

  // 每一次点击图标的事件
  cardPressHandler = (index) => {
    let isOpen = [...this.state.isOpen]

    // 如果点击的是已被翻开的图标,不执行后续代码
    if (isOpen[index]) {
      return
    }

    isOpen[index] = true

    if (this.state.firstPickedIndex == null && this.state.secondPickedIndex == null) {
      this.setState({
        isOpen,
        firstPickedIndex: index
      })
    } else if (this.state.firstPickedIndex != null && this.state.secondPickedIndex == null) {
      this.setState({
        isOpen,
        secondPickedIndex: index
      })
    }

    this.setState({
      steps: this.state.steps + 1
    })
  }

  calculateGameResult = () => {
    if (this.state.firstPickedIndex != null && this.state.secondPickedIndex != null) {
      // 判断是否所有图标都被打开(结束游戏)
      if (this.state.cardSymbolsInRand.length > 0) {
        let totalOpens = this.state.isOpen.filter((isOpen) => isOpen)
        if (totalOpens.length === this.state.cardSymbolsInRand.length) {
          this.setState({
            isEnded: true
          })
          return
        }
      }

      let firstSymbol = this.state.cardSymbolsInRand[this.state.firstPickedIndex]
      let secondSymbol = this.state.cardSymbolsInRand[this.state.secondPickedIndex]

      if (firstSymbol != secondSymbol) {
        // Incorrect
        setTimeout(() => {
          let isOpen = [...this.state.isOpen]
          isOpen[this.state.firstPickedIndex] = false
          isOpen[this.state.secondPickedIndex] = false

          this.setState({
            firstPickedIndex: null,
            secondPickedIndex: null,
            isOpen
          })
        }, 1000)
      } else {
        // Correct
        this.setState({
          firstPickedIndex: null,
          secondPickedIndex: null
        })
      }
    }
  }

  componentDidUpdate (prevProps, prevState) {
    if (prevState.secondPickedIndex != this.state.secondPickedIndex) {
      this.calculateGameResult()
    }
  }

  resetGame = () => {
    this.initGame()

    this.setState({
      firstPickedIndex: null,
      secondPickedIndex: null,
      steps: 0,
      isEnded: false
    })
  }

  render () {
    return (
      <Fragment>
        <StatusBar />
        <SafeAreaView style={ styles.container }>
          <View style={ styles.header }>
            <Text style={ styles.heading }>Matching Game</Text>
          </View>
          <View style={ styles.main }>
            <View style={ styles.gameBoard }>
              {
                this.state.cardSymbolsInRand.map((symbol, index) => {
                  return (
                    <Card
                      key={ index }
                      onPress={ () => this.cardPressHandler(index) }
                      style={ styles.button }
                      fontSize={ 30 }
                      title={ symbol }
                      cover="❓"
                      isShow={ this.state.isOpen[index] }
                    />
                  )
                })
              }
            </View>
          </View>
          <View style={ styles.footer }>
            <Text style={ styles.footerText }>{
              this.state.isEnded
                ? `Congrats! You have completed in ${ this.state.steps } setps.`
                : `You have tried ${ this.state.steps } time(s).`
            }</Text>
            {
              this.state.isEnded ?
                <TouchableOpacity onPress={ this.resetGame } style={ styles.tryAgainButton }>
                  <Text style={ styles.tryAgainButtonText }>Try Again</Text>
                </TouchableOpacity>
                : null
            }
          </View>
        </SafeAreaView>
      </Fragment>
    )
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1
  },
  header: {
    flex: 1,
    backgroundColor: '#eee',
    justifyContent: 'center',
    alignItems: 'center'
  },
  heading: {
    fontSize: 32,
    fontWeight: 'bold',
    textAlign: 'center'
  },
  main: {
    flex: 3,
    backgroundColor: '#fff'
  },
  footer: {
    flex: 1,
    backgroundColor: '#eee',
    justifyContent: 'center',
    alignItems: 'center'
  },
  footerText: {
    fontSize: 20,
    textAlign: 'center'
  },
  gameBoard: {
    flex: 1,
    flexDirection: 'row',
    flexWrap: 'wrap',
    justifyContent: 'center',
    alignItems: 'center',
    alignContent: 'center'
  },
  button: {
    backgroundColor: '#ccc',
    borderRadius: 8,
    width: 48,
    height: 48,
    justifyContent: 'center',
    alignItems: 'center',
    margin: (Dimensions.get('window').width - (48 * 4)) / (5 * 2)
  },
  buttonText: {
    fontSize: 30
  },
  tryAgainButton: {
    backgroundColor: '#ab956d',
    padding: 8,
    borderRadius: 8,
    marginTop: 20
  },
  tryAgainButtonText: {
    fontSize: 18,
    paddingHorizontal: 10,
    fontWeight: 'bold',
    color: '#fff'
  }
})

第三章 后续扩展

  1. 进行排名的统计,会涉及到在设备上的数据缓存。
  2. 通过react-router实现多路由,比如可以选择关卡等。
posted @ 2021-01-01 13:30  嗨Sirius  阅读(237)  评论(0编辑  收藏  举报