上一节我们完成了游戏的启动界面和结束界面,本节我们进入游戏场景界面的开发。游戏的一大特点是,它有丰富的背景图案,本节我们看看如何动态的去创建游戏场景的背景图案。
在实现动态背景贴图之前,我们看看静态贴图是怎么做的。在项目代码的component路径下新建一个文件叫gamescenecomponent.vue,打开该文件,编写如下代码:
<template>
<div id="floor" class="floor">
<div id="player"></div>
<div class="tile tile-1" style='transform: translate3d(100px, 0, 0)'></div>
<div class="tile tile-2" style='transform: translate3d(200px, 0, 0)'></div>
<div class="tile tile-1" style='transform: translate3d(100px, 100px, 0)'></div>
<div class="tile tile-100" style='transform: translate3d(200px, 100px, 0)'></div>
<div class="tile tile-1" style='transform: translate3d(100px, 200px, 0)'></div>
<div class="tile tile-2" style='transform: translate3d(200px, 200px, 0)'></div>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.floor {
position: relative;
width: 400px;
height: 100%;
margin: auto;
background: url(../../static/images/space-runner-bg.png) 0 0;
}
.tile {
position: absolute;
width: 100px;
height: 100px;
}
.tile-0 {}
.tile-1 {
background: url(../../static/images/runway.png);
}
.tile-2 {
background: url(../../static/images/runway2.png);
}
.tile-100 {
background: url(../../static/images/block.png);
}
.tile-4 {
background: url(../../static/images/star.png) center center no-repeat;
}
</style>
在template标签中,我们用来设计组件的界面,在里面我们添加了很多个具有tile属性的div元素,tile属性后面带有数字,不同的数字对应着不同的背景图片, div元素带有style属性,其中包含了translate3d的位置变换,我们通过设置该属性的x 和 y坐标值,用于确定div元素在页面上显示的位置,由于每个div元素对应一张背景图,于是上面的代码就相当于在页面的不同位置贴上相应的图片,多张图片组合在一起就形成了一个统一的背景。
上面代码加载进浏览器后,运行效果如下:
这里写图片描述
这种贴图办法就是静态贴图,背景图片的显示需要一行行代码来实现,这种做法很不科学和经济,倘若游戏的背景非常复杂的话,我们需要手写很多行代码才能实现,另外如果背景需要改动,那么我们必须在很多行代码中做相应修改,这么一来不但工程量很大,而且极容易出错。
下面我们看看,如何依赖VUE给我们提供的机制,方便快速的通过代码实现动态贴图。首先把template标签里面的代码删除,改成如下代码:
<template>
<div id="game-scene" class="scene">
<div id="floor" class="floor">
<div id="player"></div>
<div v-for="tile in tiles">
<div :class="tile.class" :style="tile.style">
</div>
</div>
</div>
</div>
</template>
VUE给我们听过了for循环机制,代码中v-for="tile in tiles"的意思是,从一个叫tiles的数组中取出它的每个元素,并把取出的元素命名为tile,数组tiles中含有几个元素,for循环就进行几次,假设tiles数组中含有三个元素的话,for循环执行三次,在for所在的div标签下,就会动态生成三个子div对象。tiles是来自组件的内部变量,一会我们会加以定义。
tiles是一个数组,它的包含的元素是一个Object对象,这个对象含有两个属性,一个属性叫class, 另一个属性叫style, 其中:class 和 :style是VUE给我们提供的指令,它能将html元素的class属性和style属性进行动态的绑定。结合前面代码,你或许可以猜出,tile.class 的内容应该将类似于"tile tile-1",this.style的内容将类似于"transform: translate3d(100px, 100px, 0)"。
在script标签中添加如下代码:
<script>
export default {
data () {
return {
tiles: []
}
},
....
}
我们在组件的data接口中定义并返回组件的内部变量,代码中定义了一个变量叫tiles,他是一个数组,这个变量对应的就是我们前面v-for指令中的tiles变量。继续添加如下代码:
methods: {
createTile (type, x, y) {
var tile = {}
tile.class = 'tile tile-' + type
tile.style = 'transform: translate3d(' + x + 'px, ' + y + 'px, 0)'
this.tiles.push(tile)
},
resetTiles () {
this.tiles = []
}
}
createTile 函数用来构建一个Object对象,这个对象包含两个属性,一个属性叫class,一个属性叫style,假设我们这么调用createTile函数:
this.createTile(1, 100, 0)
这相当与我们建立了一个tile对象,内容如下:
tile {
class: 'tile tile-1',
style: 'transform: translate3d(100, 0, 0);'
}
代码把上面创建好的对象压入数组tiles,VUE框架一旦发现tiles数组的内容出现了新的变化,它会重新执行 v-for="tile in tiles" 这条指令,于是在html中就会新增一个div元素,它的内容如下:
<div class="tile tile-1" style="transform: translate3d(100, 0, 0)">
</div>
大家看,这样产生的效果是不是跟我们前面手动贴图代码产生的效果是一样的。resetTiles函数实现很简单,它作用是把tiles数组清空而已。最后我们增加一个初始化函数init(),用于动态的产生游戏场景的背景图案,代码如下:
mounted () {
this.init()
},
methods: {
init () {
this.resetTiles()
this.createTile(0, 0, 0)
this.createTile(1, 100, 0)
this.createTile(2, 200, 0)
this.createTile(0, 300, 0)
this.createTile(0, 0, 100)
this.createTile(1, 100, 100)
this.createTile(2, 200, 100)
this.createTile(0, 300, 100)
this.createTile(0, 0, 200)
this.createTile(1, 100, 200)
this.createTile(100, 200, 200)
this.createTile(0, 300, 200)
this.createTile(0, 0, 300)
this.createTile(1, 100, 300)
this.createTile(2, 200, 300)
this.createTile(0, 300, 300)
},
在init函数中,通过反复的调用createTile接口,动态的在游戏场景进行背景贴图,在哪个位置贴什么样的背景图,只要修改相应的输入参数就可以了,上面的代码完成后,加载到浏览器后,运行效果如下:
这里写图片描述
可以看到,代码产生的效果跟原来静态的手动贴图是一样的,但是更灵活,也更富有效率。
游戏主循环的实现
虽然我们可以通过代码实现自动化的快速贴图,但代码完成后,图片贴到页面上后就不会再有变化了,于是我们要想游戏背景能保持时时更新,那么就必须通过代码在指定时间内,不断的修改页面上的贴图,进而实现背景的时时更新。
为了实现背景更新,我们需要在不同的时间点对不同的坐标贴上不同的背景图,为此我们需要用一个数组来表示背景图片的类型,于是在script坐标下增加如下代码:
data () {
return {
tiles: [],
runway: [[0, 1, 2, 0],
[0, 1, 2, 0],
[0, 1, 2, 0],
[0, 1, 2, 4],
[0, 1, 2, 0],
[0, 1, 2, 0],
[0, 1, 2, 0],
[0, 1, 2, 0],
[4, 1, 2, 0],
[0, 1, 2, 0],
[0, 1, 100, 0],
[0, 1, 2, 0],
[0, 1, 2, 0],
[0, 1, 2, 0],
[0, 1, 2, 0]
],
round: 0,
isGameOver: false,
runwayIndex: 0,
BOUNDARY: 1000,
TILE_WIDTH: 100,
TILE_HEIGHT: 100
}
},
通过前面代码我们知道,我们在class属性中,使用"type+数字"的方式来表示其对应的div元素应该贴上怎样的背景图,以下图片显示的是对应数组应该贴上的背景图片:
这里写图片描述
type-0 对应的是空白,也就是不在页面上贴图,type-1贴上的是黄色的左跑道,type-2贴上的是黄色的右跑道,type-10贴上的是一个雪花,type-100贴上的是跑道障碍物。runway数组里面的数字对应的就是上面的图片类型。runwayIndex用来执行runway数组中的一组数据,代码将根据runwayIdex获得要贴图的图片类型数组,然后根据数组中的数字把对应的图片贴到页面上。
TILE_HEIGHT, TILE_WIDTH 对应的是贴图的单位高度和宽度。
接着我们要在组件的methods部分增加游戏主循环的代码实现,先添加如下代码:
startOver () {
this.resetTiles()
setTimeout(this.tick, 800)
requestAnimationFrame(this.onFrame)
},
onFrame () {
this.updateTilesPosition()
requestAnimationFrame((this.onFrame).bind(this))
},
updateTilesPosition () {
for (var i = 0; i < this.tiles.length; i++) {
this.tiles[i].style = 'transform: translate3d(' + this.tiles[i].x + 'px, ' + this.tiles[i].y + 'px, 0)'
}
},
在startOver中调用requestAnimationFrame函数,传给它的参数是组件的onFrame函数,requestAnimationFrame是一个回调函数,它会把传给它的函数进行调用,所以onFrame会被requestAnimationFrame调用,在onFrame内部又调用了requestAnimationFrame,然后把自己传给它,那么这里代码的逻辑其实相当于:
while (1) {
this.updateTilesPosition()
}
我们为何不直接采用上面的循环,而是绕个弯去调用requestAnimationFrame, 然后在传递给该函数的函数中再次调用requestAnimationFrame呢?那是处于效率考虑,当我们的代码要在页面上实现复杂的动画效果时,我们要采用浏览器提供给我们的requestAnimationFrame函数,使用该函数实现循环操作,浏览器就能根据CPU的负载状况调整动画渲染操作的频繁度,让页面的动画绘制与当前CPU的负载保持一定的平衡。
在startOver函数中,我们启动了一个定时器,每800毫秒就运行tick函数,接下来我们看看该函数的实现:
ick () {
this.round += 1
this.runwayTick(this.round)
if (!this.isGameOver) {
var duration = Math.max(801 - this.round, 100)
setTimeout(this.tick, duration)
}
},
runwayTick (round) {
this.moveTilesDown()
this.runwayIndex += 1
if (this.runwayIndex >= this.runway.length) {
this.runwayIndex = 0
}
var row = this.runway[this.runwayIndex]
for (var i = 0, len = row.length; i < len; i++) {
this.createTile(row[i], i * this.TILE_WIDTH, 0)
}
},
moveTilesDown () {
for (var i = 0; i < this.tiles.length; i++) {
this.tiles[i].y += this.TILE_HEIGHT
if (this.tiles[i].y > this.BOUNDARY) {
this.tiles.splice(i, 1)
}
}
}
tick执行时,它会调用runwayTick函数,然后再启动一个定时器去回调自己,定时器会根据round变量的大小来调整回调时间,round的值也大,回调时间也就越短,也就是说tick被调用的时间就会越来越块,这个回调速度会一直加快,直到100毫秒位置。
在runwayTick中,我们调用moveTilesDown()来把页面上的背景贴图向下挪动,这样就能在页面上产生一种背景向下滚动的效果,如果某个背景图超出了页面上指定的显示范围,那么这个背景图就会从tiles数组中删除。同时我们再根据runwayIndex变量,从runway数组中取出一组数据来构造新的背景贴图。
moveTilesDown的实现逻辑是,把每一个背景图片的y坐标增加一个给定值,这样该背景图就会在页面上向下挪动一定的像素位置,多个背景图同时向下挪动时,页面上就呈现出背景向下滚动的特效。
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
这里写图片描述
网友评论