实现一个跑酷游戏

Phaser入门教程
当使用谷歌浏览器访问网页的时候,如果断网了,这时候页面就会现一个跑酷的小游戏。按下空格,这个游戏就开始了。
实现一个跑酷游戏_第1张图片
本篇教程就使用phaser来实现这个游戏。演示图如下所示。

一个简单的跑酷游戏需要一个不断移动的背景层,产生一个运动的效果。然后需要一个主角,我们的小龙。再次是道具,比如这个游戏的仙人掌和乌鸦。碰到任何一个道具游戏就结束了。
  
跟之前的教程一样,我们需要一个场景先加载进度条图片资源。一个场景加载其它所有资源,同时使用进度条显示资源的加载进度,加载完成后进入菜单场景。

// 引导
game.states.boot = function() {
    this.preload = function() {
        this.load.image('loading', 'assets/image/progress.png');
    },
    this.create = function() {
        this.state.start('preloader');
    }
}

第一个场景任务就是加载进度条图片,加载完成后切换到第二个场景。我称它为引导场景。

  

// 用来显示资源加载进度
game.states.preloader = function() {
    this.preload = function() {
        this.day = '#FFFFFF';
        game.stage.backgroundColor = this.day;
        var loadingSprite = this.add.sprite((this.world.width - 311) / 2, this.world.height / 2, 'loading');
        this.load.setPreloadSprite(loadingSprite, 0);
        this.load.atlasJSONHash('image', 'assets/image/image.png', 'assets/image/image.json');
        game.load.bitmapFont('font', 'assets/font/font_0.png', 'assets/font/font.fnt');
        this.load.audio('die', 'assets/audio/hit.mp3');
        this.load.audio('jump', 'assets/audio/press.mp3');
    },
    this.create = function() {
        this.state.start('menu');
    }
}
  • 这个场景任务是加载所有资源,同时使用第一个场景加载的进度条资源显示一个加载中的进度条。
  • game.load.bitmapFont是加载一个位图字体,第一个参数是别名,后面在创建位图字体文字对象的时候需要用到。第二个参数是位图图片,第三个参数是配套的xml文件。加载完成后启动菜单场景。

  

// 游戏菜单
game.states.menu = function() {
    this.create = function() {
        this.day = '#FFFFFF';
        game.stage.backgroundColor = this.day;
        this.land = this.add.tileSprite(0, game.height - 14, game.width, 14, 'image', 'land.png');//添加到屏幕底部
        this.dragon = this.add.sprite(0, 0, 'image', 'stand1.png')//添加小龙
        this.dragon.animations.add('stand', ['stand1.png', 'stand2.png'], 2, true, false);
        this.dragon.animations.play('stand');
        this.dragon.anchor.set(0.5);
        this.dragon.x = this.dragon.width;
        this.dragon.y = game.height - this.dragon.height / 2;
        this.tip = game.add.bitmapText(0, 0, 'font', "点击开始游戏", 28);
        this.tip.anchor.set(0.5);
        this.tip.x = game.world.centerX;
        this.tip.y = game.world.centerY;
        game.input.onDown.add(this.startGame, this);
    },
    this.startGame = function() {
        this.land.destroy();
        this.dragon.destroy();
        this.tip.destroy();
        game.state.start('start');
    }
}
  • 在这个场景上,我们使用game.stage.backgroundColor给舞台背景设置了一个白色。同时加载了陆地,和小龙的图片资源,又给小龙添加了一个动画。
  • game.add.bitmapText是创建一个位图字体文字对象,前两个参数是坐标。第三个参数是位图要使用的字体资源名称,之前设置的‘font’。第四个参数是显示的文本内容。第五个参数是字体大小。它会使用位图资源中的图片字体来显示给它设置的文本内容。

先看下这个位图资源,如下图所示。
位图字体图片

可以看到只有0-9和一些汉字还有个空格(这个空格图片上看不出来吧),需要注意的是如果给文本对象设置的文本内容里包含了位图字体上没有的字体或符号,那么运行的时候会出现异常。

实现一个跑酷游戏_第2张图片

  
现在设计最后一个也是最重要的一个场景,就是游戏场景。

  • 背景层

背景层一直在重复滚动,有一个运动的效果。可以加些云朵,让云朵稍微运动慢些。这样可以造成近的物体移动快,而远的物体移动慢的层次感效果。

this.land = this.add.tileSprite(0, game.height - 14, game.width, 14, 'image', 'land.png');
game.physics.arcade.enable(this.land);//对land对象开启物理引擎
this.land.autoScroll(this.speed, 0);//自动重复滚动
this.land.body.allowGravity = false;//不用重力
this.land.body.immovable = true;//不可移动的,别的物体碰到后会反弹

这里通过把地面图片资源加载成一个纹理图片对象,使用autoScroll来一直滚动,产生一个运动的效果。对这个对象启动物理引擎。immovable表示产生碰撞后这个对象是不动的,即不受碰撞影响。如果碰撞它的那个物体的这个属性设为false,并且设置了bounce弹性参数,那么会发生反向的位移,即反弹。

//----------------------------------云朵初始化----------------------------------
this.cloudGroup = game.add.group();//云朵分组,循环使用云朵,添加5个云朵对象,意思就是一屏幕最多5个云朵
this.cloudGroup.enableBody = true;//开启物理引擎
for (var i = 0;i < 5;i++) {
    var cloud = this.add.sprite(game.width, 0, 'image', 'cloud.png');//把云朵放到屏幕最右边
    cloud.visible = false;//默认不可见的
    cloud.alive = false;//默认是dead状态
    this.cloudGroup.add(cloud);
}
this.cloudGroup.setAll('checkWorldBounds',true); //边界检测
this.cloudGroup.setAll('outOfBoundsKill',true); //出边界后自动kill
//----------------------------------云朵初始化完成----------------------------------

初始化云朵分组,enableBody = true会使所有添加到这个分组里的对象开启物理引擎。这里云朵对象默认都是不可见的并且是dead状态。
cloudGroup.setAll(‘checkWorldBounds’,true)所有云朵都开启场景边界检测。
cloudGroup.setAll(‘outOfBoundsKill’,true)所有云朵都在运动到场景外的时候自动kill掉,转换为dead状态。

  • 道具
    道具是非常重要的一个环节,没有他们,就只剩下一个主角独舞,游戏也不会完善。如果设计过难或过于简单,都会影响整个游戏的吸引力。
    本游戏中,只有三种道具,小型的仙人掌、大型的仙人掌、乌鸦。这三种道具逻辑其实一样,就是在游戏过程中随机产生一个指定范围内的数字,根据这个数字来选择该出现一个什么道具。素材里面能看到,两种仙人掌都是3种图片,而乌鸦只有一种,可以使用两个分组来表示仙人掌。
//----------------------------------小仙人掌初始化----------------------------------
this.smallGroup = game.add.group();//小仙人掌,看素材总共有三组
this.smallGroup.enableBody = true;//开启物理引擎
//game.height - 35,其中35是仙人掌的图片高度,意思是放到屏幕最下面
var small1 = this.add.sprite(game.width, game.height - 35, 'image', 'small1.png');//仙人掌默认y坐标在屏幕最下方
var small2 = this.add.sprite(game.width, game.height - 35, 'image', 'small2.png');//仙人掌默认y坐标在屏幕最下方
var small3 = this.add.sprite(game.width, game.height - 35, 'image', 'small3.png');//仙人掌默认y坐标在屏幕最下方
small1.visible = small2.visible = small3.visible = false;//默认不可见的
small1.alive = small2.alive = small3.alive = false;//默认状态是dead
this.smallGroup.add(small1);
this.smallGroup.add(small2);
this.smallGroup.add(small3);
small1.body.setCircle(small1.width/ 2);
small2.body.setCircle(small2.width/ 2);
small3.body.setCircle(small3.width/ 2);
this.smallGroup.setAll('checkWorldBounds', true); //边界检测
this.smallGroup.setAll('outOfBoundsKill', true); //出边界后自动kill

//----------------------------------小仙人掌初始化完成----------------------------------

//----------------------------------大仙人掌初始化----------------------------------
this.bigGroup = game.add.group();//大仙人掌,看素材,同小仙人掌一样是三组
this.bigGroup.enableBody = true;//开启物理引擎
var big1 = this.add.sprite(game.width, game.height - 35, 'image', 'big1.png');//仙人掌默认y坐标在屏幕最下方
var big2 = this.add.sprite(game.width, game.height - 35, 'image', 'big2.png');//仙人掌默认y坐标在屏幕最下方
var big3 = this.add.sprite(game.width, game.height - 35, 'image', 'big3.png');//仙人掌默认y坐标在屏幕最下方
big1.visible = big2.visible = big3.visible = false;//默认不可见的
big1.alive = big2.alive = big3.alive = false;//默认状态是dead
this.bigGroup.add(big1);
this.bigGroup.add(big2);
this.bigGroup.add(big3);
big1.body.setCircle(big1.width/ 2);
big2.body.setCircle(big2.width/ 2);
big3.body.setCircle(big3.width/ 2);
this.bigGroup.setAll('checkWorldBounds', true); //边界检测
this.bigGroup.setAll('outOfBoundsKill', true); //出边界后自动kill
//----------------------------------大仙人掌初始化完成----------------------------------

//----------------------------------乌鸦初始化----------------------------------
this.bird = this.game.add.sprite(game.width, 0, 'image', 'bird1.png');
this.bird.animations.add('fly', ['bird1.png', 'bird2.png'], 10, true, false);
this.bird.visible = false;//默认不可见
this.bird.alive = false;//默认是dead状态
this.bird.checkWorldBounds = true;//检测边界
this.bird.outOfBoundsKill = true;//出了边界就变成dead,后面会重新使用
game.physics.arcade.enable(this.bird);
this.bird.body.setCircle(this.bird.height / 2);
//----------------------------------乌鸦初始化完成----------------------------------

这样3种道具就初始化完成了。关于body.setCircle这个在后面碰撞检测那里详细解释。其它的代码跟前面云朵那里很类似。

  • 定时器资源
    通过开启一个定时器,可以在一个固定的时间间隔后触发一个动作。比如每隔3秒出现一个云朵,或是每隔1秒出现一个仙人掌。也可以用定时器来计分。
game.time.events.loop(2800, this.addCloud, this); //利用时钟事件来添加云朵
game.time.events.loop(1000, this.addProps, this); //利用时钟事件来循环添加道具
game.time.events.loop(100, this.updateScore, this);//利用时钟事件更新分数

这里开启了3个定时器做不同的事。产生云朵,添加道具,计分。

  • 看看addCloud方法。
//添加云层
var cloud = this.cloudGroup.getFirstDead();
if (cloud !== null) {
    var y = game.rnd.between(cloud.height, game.height - this.dragon.height);
    cloud.reset(game.width, y);
    cloud.body.velocity.x = this.speed / 5;
}
  • 使用cloudGroup.getFirstDead()来获取分组里第一个状态是dead状态的云朵,随机一个y坐标。reset会重置云朵到指定的坐标,并且修改状态visiblealive为true(可见的和非dead的)。
  • cloud.body.velocity.x = this.speed / 5;五分一的横向运动速度会让云朵移动慢于陆地的移动,有层次感。
  • 再来看看addProps方法
//添加乌鸦还是仙人掌,随机来获取
var random = game.rnd.between(1, 100);
if (this.score >= 100) {//分数大于300后再随机道具里添加乌鸦
    if (random > 60) {
        if (this.bird.alive === false) {
            var y = game.rnd.between(this.birdMinY, this.birdMaxY);
            this.bird.reset(game.width, y);
                this.bird.body.velocity.x = this.speed;
                this.bird.animations.play('fly');
        }
    } else if (random > 30) {//大仙人掌
        var big = this.bigGroup.getFirstDead();
            if (big !== null) {
                big.reset(game.width, game.height - big.height);
                big.body.velocity.x = this.speed;
            }
    } else {//小仙人掌
        var small = this.smallGroup.getFirstDead();
            if (small !== null) {
                small.reset(game.width, game.height - small.height);
                small.body.velocity.x = this.speed;
            }
    }
} else {
    if (random < 50) {//小仙人掌
        var small = this.smallGroup.getFirstDead();
            if (small !== null) {
                small.reset(game.width, game.height - small.height);
                small.body.velocity.x = this.speed;
            }
    } else {//大仙人掌
        var big = this.bigGroup.getFirstDead();
            if (big !== null) {
                big.reset(game.width, game.height - big.height);
                big.body.velocity.x = this.speed;
            }
    }
}
  • 判断当前的分数,如果大于100分,道具会有乌鸦和大小仙人掌,这时40%的几率产生一只乌鸦,大小仙人掌各30%的几率。
  • 如果分数小于100,道具就只有大小仙人掌,各50%的几率。
  • 下面是updateScore方法。
this.score++;
this.scoreText.text = "" + this.score + "    最高 " + Math.max(this.score, this.topScore);
this.scoreText.x = game.width - this.scoreText.width - 30;

这个是时间间隔最短的定时器触发的动作。每次触发分数加1。修改分数提示文字,同时调整下它的显示位置。因为随着分数的提升,分数的位数会一直增加(10->100…),那么文字对象的宽度会增加。

  • 主角
    这个主角我称之为小龙。
//----------------------------------小龙和分数初始化----------------------------------
this.dragon = this.add.sprite(0, 0, 'image', 'stand1.png')//添加小龙
game.physics.arcade.enable(this.dragon);
this.dragon.anchor.set(0.5);
this.dragon.x = this.dragon.width;
this.dragon.y = game.height - this.dragon.height / 2;
this.dragon.body.collideWorldBounds = true;
this.dragon.animations.add('die', ['die.png'], 1, true, false);
this.dragon.animations.add('run', ['run1.png', 'run2.png'], 10, true, false);
this.dragon.animations.add('down_run', ['down_run1.png', 'down_run2.png'], 10, true, false);
this.dragon.animations.play('run');
this.dragon.body.setCircle(this.dragon.width / 2);
this.dragon.downRun = false;//是否在趴着跑
this.dragon.isJumping = false;//记录是否正在跳跃
  • 小龙有3个动画,一个是死亡时的,动画名称叫‘die’,只有一个动画帧。一个是跑动时的动画,这个动画名称‘run’,这个动画有两帧,即两幅图片。还有一个是趴下跑的动画,名称叫‘down_run’,也是两帧。游戏一开始,默认播放的是跑动动画。
  • 小龙有一个跳跃动作,有一个趴着跑的动作,这两个是冲突的。即趴着跑的时候不允许跳跃。
  • 给小龙设置this.dragon.body.collideWorldBoundstrue,会检测场景边缘,当小龙落到陆地上的时候不至于直接穿过陆地。因为没有设置弹性参数,所以小龙碰撞到陆地的时候不会反弹。
  • 这里给小龙添加两个属性(JavaScript的方便之处,想添加个属性直接添加),this.dragon.downRun记录当前是不是在趴着跑,this.dragon.isJumping记录当前是不是在跳跃过程中。

处理小龙的跳跃。

    this.jump = function() {//跳跃方法
        if (this.gameOver || this.dragon.isJumping || this.dragon.downRun) {
            return;
        }
        this.dragon.isJumping = true;//修改小龙状态为在跳跃种
        this.jumpAudio.play();//播放跳跃声音
        this.jumpTween = game.add.tween(this.dragon).to({//跳跃的运动
            y : this.dragon.y - 90
        }, 300, Phaser.Easing.Cubic.InOut, true);
        this.jumpTween.yoyo(true, 0);//反过来运动回来
        this.jumpTween.onComplete.add(this.jumpOver, this, 0, this.dragon);//运动结束回掉
    },
    this.jumpOver = function() {//跳跃完成
        this.dragon.isJumping = false;
    },
  • jump方法是小龙跳跃的方法,跳跃完成的方法是jumpOver。
    小龙跳跃是有条件约束的。首先是游戏没有结束,其次是小龙当前状态不是在跳跃中,也没有在趴着跑的状态。
  • 小龙的跳跃是用位移实现的this.jumpTween,这个位移的最终目标地址是小龙现在的y坐标减去90个像素,意思就是垂直跳起,然后再反向运动回来,这样一次来回运动完成后就算跳跃结束。
  • 这个位移结束后的回掉函数就是jumpOver,跳跃结束,修改小龙的当前状态this.dragon.isJumping为false,表示不是在跳跃中。
  • 输入处理
    这里说输入就是点击或是按键。
game.input.onDown.add(this.jump, this); //给鼠标按下事件绑定龙的跳跃动作
this.spaceKey = game.input.keyboard.addKey(Phaser.KeyCode.SPACEBAR);
this.upKey = game.input.keyboard.addKey(Phaser.KeyCode.UP);
game.input.keyboard.addKeyCapture([Phaser.KeyCode.SPACEBAR, Phaser.KeyCode.UP]);
  • 默认点击的时候触发小龙的jump方法。
  • 添加了空格、上两个按键的监听。
  • 没有添加下方向键的监听,可以在下面看到它的处理跟其它两个键的处理不一样,但是效果一样。至于喜欢哪种处理方式看个人爱好。

下面看下这三个按键的处理。

if (this.spaceKey.isDown || this.upKey.isDown) {
    this.jump();
}
if (game.input.keyboard.isDown(Phaser.KeyCode.DOWN) && this.dragon.downRun === false) {
    this.dragon.downRun = true;
    this.dragon.animations.stop();
    this.dragon.animations.play('down_run');
    this.dragon.body.setSize(this.dragon.width, this.dragon.height / 2, 0, this.dragon.height / 2);
} else if (!game.input.keyboard.isDown(Phaser.KeyCode.DOWN) && this.dragon.downRun === true) {
    this.dragon.downRun = false;
    this.dragon.animations.stop();
    this.dragon.animations.play('run');
    this.dragon.body.setCircle(this.dragon.width / 2);
    this.dragon.body.offset.set(0, 0);//恢复一下偏移为0
}
  • 空格键和上方向键按下的时候调用小龙的jump方法。
  • 下方向键按下的时候,先判断当前小龙是不是处于趴着跑的状态。如果不是,则修改小龙的状态为趴着跑,并且播放趴着跑的动画。
  • 如果下方向键没有按下,但是小龙当前处于趴着跑的状态,就修改小龙的状态为正常状态,并播放正常跑动的动画。
  • this.dragon.body.setSizethis.dragon.body.setCircle在下面碰撞检测的地方解释。
  • 碰撞检测
    这个游戏的灵魂就是碰撞检测。
if (game.physics.arcade.overlap(this.dragon, this.smallGroup) //跟小仙人掌碰撞检测
    || game.physics.arcade.overlap(this.dragon, this.bigGroup)//跟大仙人掌碰撞检测
    || game.physics.arcade.overlap(this.dragon, this.bird)) {//跟乌鸦碰撞检测
    this.smallGroup.forEach(function(item){
        item.body.stop();
    });
    this.bigGroup.forEach(function(item){
        item.body.stop();
    });
    this.bird.animations.stop();
    this.bird.body.stop();
    this.failed();
}
  • 碰撞检测方法game.physics.arcade.overlap支持精灵与精灵、精灵与分组、分组与分组之间的检测。如果发生碰撞,方法返回true。
  • 一旦发生碰撞,立即停止游戏。遍历仙人掌分组,停止所有仙人掌的移动。停止乌鸦的动画与位移(如果精灵的alive状态是false,即dead,则会忽略)。
  • 最后调用游戏结束的方法this.failed
this.failed = function() {//游戏结束
    this.die.play();//播放游戏结束的音乐
    if (this.dragon.isJumping) {
        this.jumpTween.stop();//如果正在跳跃,停止
    }
    this.dragon.animations.stop();//停止在播放的动画
    this.dragon.animations.play('die');//切换到死亡动画
    this.gameOver = true;//游戏结束
    game.time.removeAll();//停止所有定时器
    this.cloudGroup.forEach(function(item){//停止云层运动
        item.body.stop();
    });
    game.input.onDown.remove(this.jump, this);//移除之前点击事件
    game.input.onDown.add(this.gameStart, this); //添加新的点击事件
    localStorage.setItem("run_topScore", Math.max(this.score, this.topScore));//保存最高分
    this.land.stopScroll();//陆地停止运动
    this.over = this.add.sprite(0, 0, 'image', 'gameover.png');//gameover
    this.over.anchor.set(0.5);
    this.over.x = game.world.centerX;
    this.over.y = game.world.centerY - 30;
    this.restart = this.add.sprite(0, 0, 'image', 'restart.png');//restart button
    this.restart.anchor.set(0.5);
    this.restart.x = game.world.centerX;
    this.restart.y = game.world.centerY + this.over.height;
    this.restart.inputEnabled = true;//允许点击
    this.restart.input.useHandCursor = true;//鼠标移动上去显示一个手的形状
    this.restart.events.onInputDown.add(this.gameStart, this);//点击事件
}
  • failed方法里面停止云朵、陆地的运动,修改状态为游戏结束,停止所有定时器等等不适合放到update方法里来处理的都放在这里处理。
  • 添加一个gameover的图片和一个restart的图片。
  • 移除之前的点击事件,添加新的点击事件就是重新启动这个场景。同时restart图片的点击事件也是重新启动这个场景。
  • 重新启动这个场景意味着重新开始游戏。

现在来讲讲之前对精灵body属性上的处理。

  • 一个精灵只有开启物理引擎检测以后,这个body属性才有了值,不再是undefined。
  • 精灵的区域默认是整个图片,这个做碰撞检测大部分时候是非常不适合的,所以需要调整。

使用之前文章讲过的调试技巧,我们把跑动时候的小龙、乌鸦和仙人掌的body区域画出来看看。
实现一个跑酷游戏_第3张图片

相信大家一看就明白,用这么大的区域做碰撞检测可玩性会打折扣,因为空白地方太多了,显然在玩家觉得不应该碰撞的时候却发生了碰撞(图片的空白区域碰撞了)。

来调整下看看效果。

this.dragon.body.setCircle(this.dragon.width / 2);//调整小龙的body为宽的一半为半径的圆。

实现一个跑酷游戏_第4张图片

对三个都做过调整后发现好多了,仙人掌下面部分碰不到的,所以只需要上面这部分。

那当小龙趴着跑的时候怎么调整呢?
实现一个跑酷游戏_第5张图片
观察下小龙趴着跑的图片,发现大概是站着的时候一半的高度。所以这样调整下。

this.dragon.body.setSize(this.dragon.width, this.dragon.height / 2, 0, this.dragon.height / 2);

实现一个跑酷游戏_第6张图片

  • this.dragon.body.setSize会修改小龙的body区域,前两个参数是宽高,这里修改为高度的一半,后两个参数是坐标轴上的偏移,x轴上没有偏移,y轴上向下偏移了高度的一半。
  • 从趴着跑恢复到正常跑动状态的时候需要把偏移恢复了,不然图片位置会出现问题。

大家可以添加一些积极的道具比如增加分数、添加金币等等。
这个游戏教程到这里就基本讲完了。后面可能会出一个类型rpg的游戏教程。
本次教程源码地址:
码云
github

你可能感兴趣的:(Phaser,JavaScript,Html5)