基于ReactNative的跨平台俄罗斯方块游戏的实现3——游戏中Shape的实现

上一篇中,我记录了基于React Native的俄罗斯方块小游戏的游戏框架的搭建,本篇记录的是如何实现俄罗斯方块的Shape,Shape代表一个可以移动和下落的图形。

ART绘图基础

要完成俄罗斯方块的Shape,必须先了解下React Native中的绘图基础。React Native内置了一个ART库专门用于绘图,下面简单记录一下使用ART库的方法。下面的代码用于在手机屏幕的中央绘制一个边长为100像素的正方形:

import React, { Component } from 'react';
import {
  StyleSheet,
  View,
  ART,
  Dimensions
} from 'react-native';

type Props = {};
export default class App extends Component<Props> {
  render() {
    const path = ART.Path();
    path.moveTo(0, 0);
    path.lineTo(100, 0);
    path.lineTo(100, 100);
    path.lineTo(0, 100);
    path.lineTo(0, 0);
    path.close();
    return (
      <View style={styles.container}>
        <ART.Surface width={100} height={100}>
          <ART.Shape d={path} fill="#6699FF" stroke="#FF0000" strokeWidth={2}/>
        </ART.Surface>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

代码运行的效果如下图:

绘制正方形的步骤如下:
1. 导入ART包:import {ART} from 'react-native';
2. 创建path对象,通过path对象确定图形的轮廓,如下代码:
const path = ART.Path();
path.moveTo(0, 0);
path.lineTo(100, 0);
path.lineTo(100, 100);
path.lineTo(0, 100);
path.lineTo(0, 0);
path.close();
3. 使用<ART.Surface><ART.Shape>绘制图形。这里需要注意的是,<ART.Shape>组件必须包裹在<ART.Surface>组件中,通过给<ART.Shape>设置d={path}属性确定图形的形状,fill属性指定了图形的填充颜色,stroke属性指定的图形的边框颜色,strokeWidth指定了图形的边框宽度。

Shape类的设计与实现

有了ART的绘图基础后,我们再来完成Shape类的设计与实现。

Shape类的设计

一个Shape类代表了一个可以左右移动,可以变形和下落的图形,在游戏中,图形有很多不同的形状,比如长条、方块、弯的等等。我们用一个4x4的二维数组来表示一个图形的结构,数组中通过0和1来表示空心和实心,在实心的地方通过ART绘图,在空心的地方不绘制任何东西,以此来完成Shape类的设计,如下面的二维数组:

[1, 1, 1, 0],
[0, 1, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]

就表示一个T形的方块。

Shape类的编码实现

设计完Shape类后需要编码实现这个类,Shape有一些属性比如:起始坐标、Shape的二维数组数据,Shape还有一些方法比如:左移、右移、变形、自动下落、Shape触到Ground后的处理。

Shape的变形

这里先考虑Shape的变形,以T形为例,一个T形图形可以变形为倒的T形或者偏的T形,为了处理图形的变形,我们用一个数组来存放某个图形的所有可能的形状,这就变成了一个三维数组,如下所示:

let shapeData = [[
  [1, 0, 0, 0],
  [1, 1, 0, 0],
  [1, 0, 0, 0],
  [0, 0, 0, 0]
], [
  [1, 1, 1, 0],
  [0, 1, 0, 0],
  [0, 0, 0, 0],
  [0, 0, 0, 0]
], [
  [0, 0, 1, 0],
  [0, 1, 1, 0],
  [0, 0, 1, 0],
  [0, 0, 0, 0]
], [
  [0, 0, 0, 0],
  [0, 1, 0, 0],
  [1, 1, 1, 0],
  [0, 0, 0, 0]
]]

初始情况下,图形为上面shapeData[0]的数据,当图形需要变形时,就变为shapeData[1]的数据,再需要变形时,就变为shapeData[2]的数据,依次循环。

Shape的左右移动和下落

Shape的左右移动比较简单,改变Shape的坐标就行,这里我们为Shape设置一个起始坐标点(posX, posY),存放在state中,当我们改变state中的这个坐标时,Shape会自动重新绘制,从而达到移动的效果,Shape移动的代码如下:

  moveLeft() {
    this.setState({posX: this.state.posX - 1});
  }
  moveRight() {
    this.setState({posX: this.state.posX + 1});
  }
  moveDown() {
    this.setState({posY: this.state.posY + 1});
  }

Shape的自动下落

我们不做任何操作的时候,一个图形是会自动下落的,这里我们需要使用一个定时任务让图形能够自动下落,但是每一次下落都需要做判断,如果Shape触到了Ground,就认为这个Shape死亡,被Ground吃掉,这时候要关闭让Shape自动下落的定时任务,并且还要判断游戏是否结束,如果游戏没有结束,需要产生新的Shape并自动下落,这里的逻辑稍微复杂点,要判断的条件比较多,先放上Shape自动下落的代码:

autoMoveDown() { // 自动下落
  this.moveFun = ()=>{ // 这是定时执行的方法,用于驱动图形自动下落
    if (this.props.canShapeMoveDown && this.props.canShapeMoveDown()) { // 如果图形可以下落,则下落
      this.setState({
        posX: this.state.posX,
        posY: 1 + this.state.posY
      });
    } else {
      // 不能下落了,清除定时任务
      this._clearInterval();
      // 触发图形死亡的回调
      this.props.onShapeDieListener && this.props.onShapeDieListener(this.state.posX, this.state.posY, shapeArrData[shapeIndex]);
      // 判断是否游戏结束,结束条件是shape被吃时的posY坐标跟初始的posY坐标相同
      if (this.state.posY == this.initY) {
        // 游戏结束
        this.props.onGameOverListener && this.props.onGameOverListener();
      } else {
        // 游戏未结束
        this._reset();
      }
      return ;
    }
  }
  this.intervalId = setInterval(this.moveFun, 600);
}

上面的代码中涉及到一些在<GamePlayingView>组件中的方法,比如判断图形是否可以下落的方法canShapeMoveDown,图形被Ground吃掉后死亡的回调方法onShapeDieListener,这些将在后续的篇幅中记录。

Shape的绘制

在React Native中,我们需要实现组件的render方法来完成组件的渲染,这里我们如何渲染一个Shape图形呢?由于Shape的数据就是一个整型的二维数组,所以我们可以通过一个循环来遍历这个数组,将这个数组中值为1的地方画出颜色,将值为0的地方画透明色,这样就组成了一个完整的图形。绘图主要是下面两个方法:

// 生成一个小方格(4x4中的某一个格子),根据fill判断是否填充颜色
generateCell(posX, posY, indexX, indexY, fill) { 
  const path = ART.Path();
  path.moveTo((posX + indexX) * WIDTH, (posY + indexY) * WIDTH);
  path.lineTo((posX + indexX + 1) * WIDTH, (posY + indexY) * WIDTH);
  path.lineTo((posX + indexX + 1) * WIDTH, (posY + indexY + 1) * WIDTH);
  path.lineTo((posX + indexX) * WIDTH, (posY + indexY + 1) * WIDTH);
  path.lineTo((posX + indexX) * WIDTH, (posY + indexY) * WIDTH);
  path.close();
  if (fill) {
     // 值为1的处理
    return <ART.Shape d={path} fill="#33CC99" stroke="#6699CC" strokeWidth={DIVIDER_WIDTH} />
  } else {
     // 值为0的处理
    return <ART.Shape d={path} fill="#00000000" />
  }
}

// 生成一个形状(4x4的整个图形)
generateShape(shapeArr) { 
  let shape = [];
  // 遍历二维数组,将所有的小格子存起来
  for (let row = 0; row < 4; row++) {
    for (let col = 0; col < 4; col++) {
      shape.push(this.generateCell(this.state.posX, this.state.posY, col, row, shapeArr[row][col]));
    }
  }
  return shape;
}

最后我们的render方法,就只需要调用上面的generateShape方法就行了。下面放上Shape类的完整代码:

import React, { Component } from 'react';
import {ART, View} from 'react-native';
import ShapeFactory from './ShapeFactory';
import {CELL_HOR_COUNT, WIDTH, DIVIDER_WIDTH} from '../utils/Constants';

let shapeIndex = 0;
const shapeFactory = new ShapeFactory();
let shapeArrData = shapeFactory.getRandomShapeData();

export default class Shape extends Component {
  constructor(props) {
    super(props);
    this.initX = parseInt(CELL_HOR_COUNT / 2) - 2;
    this.initY = 5;
    this.state = { // 初始坐标
      posX: this.initX,
      posY: this.initY
    }
  }
  getPosition() { // 返回Shape当前的坐标
    return {
      posX: this.state.posX,
      posY: this.state.posY
    };
  }
  getData() { // 返回Shape的二维数组数据
    return shapeArrData[shapeIndex];
  }
  getNextShapeData() { // 返回Shape的下一个形状的二维数组数据
    return shapeArrData[(shapeIndex + 1) % 4];
  }
  _reset() { // 被Ground吃掉后重置
    shapeArrData = shapeFactory.getRandomShapeData();
    shapeIndex = 0;
    this.setState({
      posX: this.initX,
      posY: this.initY
    }, ()=>{
      this.autoMoveDown();
    });
  }
  generateCell(posX, posY, indexX, indexY, fill) { // 生成一个小方格,根据fill判断是否填充颜色
    const path = ART.Path();
    path.moveTo((posX + indexX) * WIDTH, (posY + indexY) * WIDTH);
    path.lineTo((posX + indexX + 1) * WIDTH, (posY + indexY) * WIDTH);
    path.lineTo((posX + indexX + 1) * WIDTH, (posY + indexY + 1) * WIDTH);
    path.lineTo((posX + indexX) * WIDTH, (posY + indexY + 1) * WIDTH);
    path.lineTo((posX + indexX) * WIDTH, (posY + indexY) * WIDTH);
    path.close();
    if (fill) {
      return <ART.Shape d={path} fill="#33CC99" stroke="#6699CC" strokeWidth={DIVIDER_WIDTH} />
    } else {
      return <ART.Shape d={path} fill="#00000000" />
    }
  }
  generateShape(shapeArr) { // 生成一个形状
    let shape = [];
    for (let row = 0; row < 4; row++) {
      for (let col = 0; col < 4; col++) {
        shape.push(this.generateCell(this.state.posX, this.state.posY, col, row, shapeArr[row][col]));
      }
    }
    return shape;
  }
  autoMoveDown() { // 自动下落
    this.moveFun = ()=>{
      if (this.props.canShapeMoveDown && this.props.canShapeMoveDown()) {
        this.setState({
          posX: this.state.posX,
          posY: 1 + this.state.posY
        });
      } else {
        // 不能下落了
        this._clearInterval();
        this.props.onShapeDieListener && this.props.onShapeDieListener(this.state.posX, this.state.posY, shapeArrData[shapeIndex]);
        // 判断是否游戏结束,结束条件是shape被吃时的posY坐标跟初始的posY坐标相同
        if (this.state.posY == this.initY) {
          // 游戏结束
          this.props.onGameOverListener && this.props.onGameOverListener();
        } else {
          // 游戏未结束
          this._reset();
        }
        return ;
      }
    }
    this.intervalId = setInterval(this.moveFun, 600);
  }
  componentWillMount() { // 即将挂载组件时,开启定时任务让Shape自动下落
    this.autoMoveDown();
  }
  render() { // 渲染Shape
    return this.generateShape(shapeArrData[shapeIndex % 4]);
  }
  transform() { // 图形变形
    shapeIndex = (++shapeIndex) % 4;
  }
  moveLeft() {
    this.setState({posX: this.state.posX - 1});
  }
  moveRight() {
    this.setState({posX: this.state.posX + 1});
  }
  moveDown() {
    this.setState({posY: this.state.posY + 1});
  }
  _clearInterval() { // 清除定时任务
    this.intervalId && clearInterval(this.intervalId);
  }
  componentWillUnmount() { // 组件即将卸载的时候,停掉定时任务
    this._clearInterval();
  }
}

后续

下一篇将记录Ground类的设计与编码实现。

源码

点击查看项目源码-GitHub
点击查看项目源码-码云

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yubo_725

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值