在上一篇中,我记录了基于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类的设计与编码实现。