【RPG Maker MV插件编程】【实例教程5】制作小游戏:坦克大战(上)

  • 作者:Mandarava(鳗驼螺)
  • 微博:@鳗驼螺pro

MV中的小游戏可以用事件来做,也可以用插件来写。本文将使用插件的方式编写一个类似于坦克大战的游戏。本文的目的并非展示如何制作一个完整的坦克大战游戏,而是学习如何使用MV的插件来编写小游戏。所以本文的重点是介绍制作MV小游戏可能用到的相关方法,希望抛砖引玉,参考本文的实现方式,能让各位编写出各种类型的其它小游戏。

本文所涉及的内容:

  1. 游戏结构及流程介绍
  2. 相关素材资源的下载和使用
  3. 基础知识:音效的播放
  4. 基础知识:精灵表的切帧
  5. 基础知识:使用MV中的动画
  6. Scene_TankWarTitle类解析
  7. Sprite_Bullet类解析
  8. Sprite_Explode类解析
  9. Sprite_Tank类解析
  10. Sprite_Enemy类解析
  11. Scene_TankWar类解析
  12. Scene_TankWarGameOver类解析

游戏结构及流程介绍

整个坦克大战游戏包含三个场景:标题场景(Scene_TankWarTitle类)、战场场景(Scene_TankWar类)、结束场景(Scene_TankWarGameOver类)。在标题场景,按确定键或点击画面将进入战场场景,展开坦克对战。在战场场景玩家坦克出生在场景中间靠底部的位置(在MV的场景中,画面的左上角为坐标原点),敌人在顶部的随机位置出生,出生时带有类似传送效果的动画(该动画来自MV数据库中的自带动画)。同屏最多显示4辆敌人坦克,每消灭一辆就会随即出生一辆新的,整场战斗最多出生20辆敌人坦克。敌人坦克拥有简单的AI,可以随机移动、开火。默认敌人坦克每辆只有1HP生命值,玩家坦克则为2HP;坦克每次被炮弹击中将损伤1HP生命值。当玩家消灭所有20辆坦克,或被坦克消灭后,将进入结束场景,根据玩家的输赢,显示“你输了”或“你赢了”的图片文字。效果如下:

Preview.gif

为了开发与调试的快捷,可以重写Scene_Boot.prototype.start方法,使游戏在运行时直接进入坦克大战游戏的标题画面Scene_TankWarTitle(当然,最终发布时,你可以在MV地图上添加一个NPC事件,然后调用脚本命令SceneManager.goto(Scene_TankWarTitle);来打开坦克大战小游戏),代码如下:

var Scene_Boot_start = Scene_Boot.prototype.start;
Scene_Boot.prototype.start = function () {
    Scene_Base.prototype.start.call(this);
    SoundManager.preloadImportantSounds();
    if (DataManager.isBattleTest()) {
        DataManager.setupBattleTest();
        SceneManager.goto(Scene_Battle);
    } else if (DataManager.isEventTest()) {
        DataManager.setupEventTest();
        SceneManager.goto(Scene_Map);
    } else {
        this.checkPlayerLocation();
        DataManager.setupNewGame();
        SceneManager.goto(Scene_TankWarTitle);//MV游戏启动时直接进入坦克大战游戏的标题画面
        Window_TitleCommand.initCommandPosition();
    }
    this.updateDocumentTitle();
};

相关素材资源的下载和使用

本文所涉及的图片、音效资源请在 这里 下载。mndtankwar 文件夹请放到MV项目目录下的img文件夹下,se文件夹中的音频文件放到项目的audio/se文件夹下。
  为了方便的加载资源图片,先扩展ImageManager类,定义一个loadTankwar方法,该方法用于从 img/mndtankwar 文件夹中加载指定名称的图片。

ImageManager.loadTankwar = function (filename, hue) {
    return ImageManager.loadBitmap("img/mndtankwar/", filename, hue, false);
};

本文的案例游戏所涉及的图片、音效等素材资源均来源于网络,切勿用于商业用途。

基础知识:音效的播放

使用AudioManager.playSe(se)来播放音效,音效文件存放于项目的audio/se/目录下。该方法的参数se并不是单纯的音频文件名称,而是一个指定了特定属性的对象,这个对象中需要指定音频文件名namepan(这可能是有关声道均衡的值,-1完全左声道,1完全右声道,0表示平衡,其它值为双声道混合?!音频专业词汇我不确定用途;有兴趣可以参考这里),音高pitch,音量volume。比如以播放TankWarStart音频为例,se的格式如下:

var se={
    name:"TankWarStart",    //音频文件名
    pan:0,                  //pan值
    pitch:100,              //pitch音高值
    volume:100              //volume音量值
};
AudioManager.playSe(se);

这个方法主要用于播放音效,在本游戏中,比如播放开火、爆炸等的音效。

基础知识:精灵表的切帧

精灵表是将精灵动画序列的各帧图片合成在一张图片上,便于管理和运行时减少内存等资源占用。切帧的目的,就是要记录各个帧图片在大图中的位置(坐标)、宽、高等信息,以便在我们需要绘制精灵的不同帧图片时可以通过这些信息快速的取得要绘制的帧图片在大图中的位置区域。
  下面是我们游戏中使用的玩家坦克的精灵表(TankPlayer.png)。可以看到,该精灵表由4x4=16张帧图片构成,每张图片都是同等大小,规则排列。该大图尺寸为160x160,那么每个帧图片的尺寸就是40x40。
  该图片上第一排是坦克向下运动时的行走动画帧,第二排是向左运动时,第三排是向右运动时,第四排是向上运动时。

玩家坦克精灵表 TankPlayer.png

  从这张图片很容易知道每个帧图片在这张大图上的位置和高宽尺寸。首先,各帧图片的高宽尺寸前面说了都是40x40,所以第一行第1帧的位置及宽度信息就(帧信息)是:{x: 0, y: 0, width: 40, height: 40},也就是坐标为(0,0),宽高为40x40;第一行第2帧就是:{x: 40, y: 0, width: 40, height: 40},第二行第1帧就是:{x: 0, y: 40, width: 40, height: 40},第二行第2帧就是:{x: 40, y: 40, width: 40, height: 40}…以此类推。我们把它总结成一个makeAnimFrames全局函数,代码如下:

/**
 * 精灵表(Sprite Sheet)切帧方法
 * @param texture 精灵表图片
 * @param frameWidth 帧图片的宽度
 * @param frameHeight 帧图片的高度
 * @returns {Array} 帧信息(帧图片在精灵表图片中的坐标、宽度、高度信息)数组
 */
function makeAnimFrames(texture, frameWidth, frameHeight) {
    var rows=parseInt(texture.height/frameHeight);  //包含的帧图片行数
    var cols=parseInt(texture.width/frameWidth);    //包含的帧图片列数
    var animFrames = [];//二维数组,对应于精灵表的各行各列中的每一帧,其每个元素用于存储每行的所有帧信息
    for(var row=0;row<rows;row++) {
        animFrames.push([]);//二维数组的每个元素是一个一维数组
        for (var col=0;col<cols;col++) {
            var frame={ //帧信息,格式如:{x: 0, y: 0, width: 40, height: 40},表示该帧图片在精灵表中的坐标及尺寸信息
                x: col * frameWidth,
                y: row * frameHeight,
                width: frameWidth,
                height: frameHeight
            };
            animFrames[row].push(frame);//一维数组的每个元素是一个frame帧信息
        }
    }
    return animFrames;
}

参数texture是精灵图片,frameWidthframeHeight表示帧图片的宽高尺寸。那么要对上面这张玩家坦克精灵表切帧就可以使用下面的方法:

var texture = ImageManager.loadTankwar("TankPlayer");
//...等待texture加载完毕...
var animFrames = makeAnimFrames(texture, frameWidth, frameHeight)

这样得到的animFrames是个二维数组,其中的每个元素则是一个一维数组,共4个一维数组,各自存储了坦克四方行走动画的每帧的帧信息。
  PS:ImageManager.loadXXX加载图片的方法并不能立即加载完图片,这里只是个演示,通常我们会在Scene的create方法中加载图片资源,然后至少在start方法中再去切帧。

那么这样切帧以后有什么用呢?这就涉及到MV中的另一个方法:Sprite.prototype.setFrame(x, y, width, height)这个方法的作用是:在绘制精灵图片到屏幕时,它并不是直接绘制整个图片,而是只绘制图片中的一部分,具体绘制哪一部分则由参数x, y, width, height来决定。它的决定方式是:从精灵表图片上的坐标(x,y)(左上角为原点)处开始向右取width宽,向下取height高,最终围成的区域。如下图,这张精灵表图片尺寸为144x192,由3x4帧组成,每帧图片的尺寸是48x48,如果将参数设置为x=48, y=48, width=48, height=48,那么使用setFrame绘制图片时只会绘制下图中由绿色块围成的区域内的帧图片。回头看上文中定义的makeAnimFrames方法,该方法中最后取得的帧信息是这样定义的:var frame={ x: col * frameWidth, y: row * frameHeight, width: frameWidth, height: frameHeight };,现在应该更容易明白我为什么这样定义帧信息结构了:便于与setFrame中的参数相对应。

setFrame示例

  当然,setFrame并不是最终目的,我们的最终目的是通过在更新方法update中不断调用setFrame来显示动画序列的各帧图片制作成对象的动画效果(update方法由游戏引擎每帧调用一次,不需要关心它什么时候被调用,更不需要手动去调用它),如下图,这是玩家坦克的动画效果(红灯闪烁,使用前文的“玩家坦克精灵表 TankPlayer.png”):
动画

基础知识:使用MV中的动画

在小游戏中也可以直接调用MV数据库定义好的动画效果,其方法也很简单,首先取得动画的实例,然后使用一个Sprite类的精灵对象,调用startAnimation(animation, mirror, delay)方法即可。如下代码:

var animation = $dataAnimations[46];
sprite.startAnimation(animation, false, 0);

使用$dataAnimations[animationId]获取MV数据库中定义的指定动画id的动画。这里的animationId具体值可以在MV【数据库 - 动画】中查看(要用哪个动画,这里就填它的id),这里就是id为46的动画。取得动画后就调用sprite.startAnimation(animation, false, 0)来展示动画。这里的另外二个参数,一个是mirror,表示是否让动画镜像播放,delay表示延迟播放的时间。

Scene_TankWarTitle类解析

/**
 * 坦克大战游戏标题画面场景
 * @constructor
 */
function Scene_TankWarTitle() {
    this.initialize.apply(this, arguments);
};

Scene_TankWarTitle.prototype = Object.create(Scene_Base.prototype);
Scene_TankWarTitle.prototype.constructor = Scene_TankWarTitle;

Scene_TankWarTitle.prototype.create = function () {
    Scene_Base.prototype.create.call(this);

    this._backgroundSprite=new Sprite(ImageManager.loadTankwar("TitleBack"));//显示背景的精灵
    this.addChild(this._backgroundSprite);  //将背景加入场景

    this._logo=new Sprite(ImageManager.loadTankwar("Logo"));//显示Logo的精灵
    this._logo.anchor=new Point(0.5,0.5);   //设置锚点到其正中心
    this._logo.x=Graphics.boxWidth/2;       //设置Logo的x坐标
    this._logo.y=Graphics.boxHeight/2;      //设置Logo的y坐标
    this.addChild(this._logo);
};

Scene_TankWarTitle.prototype.update = function () {
    if(Input.isTriggered('ok') || TouchInput.isTriggered()){//当玩家按下确定键或点击屏幕
        SceneManager.goto(Scene_TankWar);   //进入游戏主场景:战场场景
    }
};

这个类对应于标题场景。这个类比较简单,在create方法中向场景添加背景精灵和一个游戏标题Logo精灵;在update方法中检测用户是否按下确定键或点击了屏幕,如果是,则进入战场场景Scene_TankWar

Sprite_Bullet类解析

在解析最重要的Scene_TankWar类之前,先将一些它需要用到的类创建出来。

/**
 * 坦克炮弹类
 * @constructor
 */
function Sprite_Bullet() {
    this.initialize.apply(this, arguments);
};

Sprite_Bullet.prototype = Object.create(Sprite_Base.prototype);
Sprite_Bullet.prototype.constructor = Sprite_Bullet;

Sprite_Bullet.prototype.initialize = function (texture) {
    Sprite_Base.prototype.initialize.call(this);

    this.bitmap = texture; //设置炮弹精灵的图片
    this.velocity = new Point(0, 0); //炮弹的前进速度
};

Sprite_Bullet这个类是指坦克打出的炮弹(子弹)。在initialize方法中指定精灵图片,并设置其默认的初始速度,这个初始速度并不重要,在坦克进行开火fire时,会重新分配一个速度,包括速度方向。

Sprite_Explode类解析

/**
 * 爆炸火球(效果)类
 * @constructor
 */
function Sprite_Explode() {
    this.initialize.apply(this, arguments);
};

Sprite_Explode.prototype = Object.create(Sprite_Base.prototype);
Sprite_Explode.prototype.constructor = Sprite_Explode;

Sprite_Explode.prototype.initialize = function (texture) {
    Sprite_Base.prototype.initialize.call(this);

    this._animFrames = makeAnimFrames(texture, 128, 128)[0]; //爆炸效果使用精灵表Explode.png,其尺寸1024x128,共1行8帧图片,所以每帧的宽高为128x128
    this._desireTick = 6;   //爆炸动画二帧之间的间隔时间
    this._tick = 0;         //爆炸动画绘制上一帧之后流逝的时间
    this._currentAnimFrameIndex = 0; //当前的动画帧索引
    this.isFinished = false;    //爆炸动画是否播放完毕(8帧)
    this.bitmap = texture;      //设置精灵的纹理图片
    this.updateCurrentFrame();  //更新当前的帧图片
};

/**
 * 更新显示当前的动画帧图片
 */
Sprite_Explode.prototype.updateCurrentFrame = function () {
    var frame = this._animFrames[this._currentAnimFrameIndex];
    this.setFrame(frame.x, frame.y, frame.width, frame.height);
};

Sprite_Explode.prototype.update = function () {
    Sprite_Base.prototype.update.call(this);

    this._tick++;
    if(this._currentAnimFrameIndex>=this._animFrames.length-1){ //如果8帧都播放完毕,则:
        this.isFinished=true; //标记为动画结束
    }else { //否则当流逝时间到达指定时间后更新下一帧图片
        if (this._tick >= this._desireTick) {
            this._tick = 0;
            this.updateCurrentFrame();
            this._currentAnimFrameIndex++;
        }
    }
};

这个是坦克被击中发生爆炸时的爆炸动画类。在initialize方法中设置精灵图片,初始化其动画序列的帧信息。爆炸的精灵表如下图,它是一个由8帧图片组成的动画序列。在update方法中,在每次到达一个固定时间后让帧索引递增,使用this.updateCurrentFrame()方法更新当前要绘制的帧图片(当然实际还是调用setFrame方法来绘制,像前文所述的那样)。爆炸效果在这8帧动画播放完毕后就会使用this.isFinished=true;将自己标记为“动画已经结束”。这样战场场景Scene_TankWar就会知道这个爆炸效果已经结束,可以把它删除了。

Explode.png

Sprite_Tank类解析

/**
 * 坦克类
 * @constructor
 */
function Sprite_Tank() {
    this.initialize.apply(this, arguments);
};

Sprite_Tank.prototype = Object.create(Sprite_Base.prototype);
Sprite_Tank.prototype.constructor = Sprite_Tank;

Sprite_Tank.prototype.initialize = function (texture, frameWidth, frameHeight, hp) {
    Sprite_Base.prototype.initialize.call(this);

    this.canFire = true;  //能否开火(火炮冷却后才能再次开火)
    this.speed = 0;       //移动速度
    this.hp = hp;         //HP生命值
    this.state = Tank_State.Live; //状态
    this._animFrames = makeAnimFrames(texture, frameWidth, frameHeight);//包含了四个方向(向下、向左、向右、向上)的行走动画
    this._desireMoveTick = 20;   //坦克的移动动画二帧间隔的时间
    this._moveTick = 0;          //移动动画的当前流逝的时间
    this._desireFireTick = 30;   //坦克开火后二次开火间的间隔时间
    this._fireTick = 0;          //坦克自上次开火后流逝的时间
    this._currentAnimFrameIndex = 0; //移动动画的当前帧
    this._desireDieTick = 30;    //坦克从死亡到已经死亡所需要的时间
    this._dieTick = 0;           //死亡开始后流逝的时间
    this.anchor = new Point(0.5, 0.5); //设置锚点为其正中心
    this.bitmap = texture;       //设置其纹理图片
    this.look(Direction.Up);     //默认面向上
};

/**
 * 当前动画的帧序列信息数组
 */
Object.defineProperty(Sprite_Tank.prototype, 'currentAnimFrames', {
    get: function() {
        return this._animFrames[this._direction];
    }
});

/**
 * 让坦克面向指定的方向
 * @param direction
 */
Sprite_Tank.prototype.look = function (direction) {
    if(this._direction != direction) {
        this._direction = direction;
        this._currentAnimFrameIndex = 0;
        this.updateCurrentFrame();
    }
};

/**
 * 坦克开火
 * @param texture
 * @returns {Sprite_Bullet}
 */
Sprite_Tank.prototype.fire = function (texture) {
    this.canFire=false; //开过火后需要一段时间再开火,所以设置canFire=false
    var bullet=new Sprite_Bullet(texture);
    bullet.anchor=new Point(0.5,0.5);//将锚点设置到底部(左上角为原点)
    var bulletSpeed=10;
    switch (this._direction) {
        case Direction.Down:
            bullet.rotation = -180 * Math.PI / 180;     //由于炮弹的素材是个长方形的,所以要旋转炮弹让长边顺向开火方向
            bullet.x=this.x;                //将炮弹的初始x位置放到坦克的x位置
            bullet.y=this.y+this.height/2;  //将炮弹的初始y位置放到坦克的前方(这样就像是从炮筒射击出来的一样)
            bullet.velocity=new Point(0, bulletSpeed);  //根据坦克的面向,设置炮弹的前进方向和速度
            break;
        case Direction.Left:
            bullet.rotation = -90 * Math.PI / 180;
            bullet.x=this.x-this.width/2;
            bullet.y=this.y;
            bullet.velocity=new Point(-bulletSpeed, 0);
            break;
        case Direction.Right:
            bullet.rotation = 90 * Math.PI / 180;
            bullet.x=this.x+this.width/2;
            bullet.y=this.y;
            bullet.velocity=new Point(bulletSpeed, 0);
            break;
        case Direction.Up:
            bullet.rotation = 0;
            bullet.x=this.x;
            bullet.y=this.y-this.height/2;
            bullet.velocity=new Point(0, -bulletSpeed);
            break
        default: break;
    }
    AudioManager.playSe({ //播放一个开火音效 TankWarFire.ogg / TankWarFire.m4a
        name:"TankWarFire",
        pan:0,
        pitch:100,
        volume:100
    });
    return bullet;
};

/**
 * 移动坦克
 */
Sprite_Tank.prototype.move = function () {
    switch (this._direction){//移动时要根据坦克朝向
        case Direction.Down:
            this.y += this.speed;
            break;
        case Direction.Left:
            this.x -= this.speed;
            break;
        case Direction.Right:
            this.x += this.speed;
            break;
        case Direction.Up:
            this.y -= this.speed;
            break;
    }
};

/**
 * 坦克受到伤害
 * @param damage 坦克受到的伤害值
 */
Sprite_Tank.prototype.hurt = function (damage) {
    if(this.state == Tank_State.Live) {
        this.hp = Math.max(0, this.hp - damage);
        if (this.hp <= 0) {         //如果伤害后没有了hp生命值,则:
            this.canFire = false;   //死亡后不允许再开火
            this.state = Tank_State.Dying; //坦克开始死亡(坦克从开始死亡到完全死亡有一个很短的时间,用于等待爆炸效果动画)
        }
    }
};

/**
 * 更新显示当前的动画帧图片
 */
Sprite_Tank.prototype.updateCurrentFrame = function () {
    var frame=this.currentAnimFrames[this._currentAnimFrameIndex];  //取得当前要绘制的帧图片的帧信息
    this.setFrame(frame.x, frame.y, frame.width, frame.height);     //绘制指定帧的图片:根据帧信息,找到精灵表中对应帧信息坐标、宽高的帧图片,并绘制到屏幕
};

Sprite_Tank.prototype.update = function () {
    Sprite_Base.prototype.update.call(this);

    switch (this.state) {
        case Tank_State.Live: //如果坦克状态是:活着
            this._moveTick++;
            if (this._moveTick >= this._desireMoveTick) {//当流逝时间到达指定的时间后更新坦克的帧图片(用于展示坦克的行驶动画)
                this._moveTick = 0;
                this._currentAnimFrameIndex = this._currentAnimFrameIndex % this.currentAnimFrames.length;
                this.updateCurrentFrame();
                this._currentAnimFrameIndex++;
            }

            if (!this.canFire) { //如果坦克当前不能开火
                this._fireTick++;
                if (this._fireTick >= this._desireFireTick) {//当流逝时间到达指定的时间后,重新允许坦克可开火(模拟开火冷却效果)
                    this._fireTick = 0;
                    this.canFire = true;
                }
            }
            break;
        case Tank_State.Dying: //如果坦克状态是:正在死亡
            this._dieTick++;
            if (this._dieTick >= this._desireDieTick) {//当流逝时间到达指定时间后,“正在死亡”的状态结束,坦克变成“已经死亡”
                this.state = Tank_State.Dead;
            }
            break;
        case Tank_State.Dead: //如果坦克状态是:已经死亡
            break;
        default:
            break;
    }
};

这个是坦克类,也作为玩家坦克类。坦克有朝向、行驶速度、HP生命值、状态State等。
  坦克可以前进、开火、转向。方向有四种(这四个方向正好与我们的坦克精灵表上的四方行走动画及它们的排列顺序相一致,对于人物四方行走动画的精灵表,往往是第一行是向下的行走动画,第二行是向左的,第三行是向右的,第四行是向上的),我们需要定义一个方向“枚举”:

var Direction = {
    Down: 0,    //向下
    Left: 1,    //向左
    Right: 2,   //向右
    Up: 3       //向上
};

坦克有三种状态:活着、正在死亡、已经死亡,也需要定义一个枚举:

var Tank_State = {
    Live: 0,    //活着
    Dying: 1,   //死亡中
    Dead: 2     //死亡
}

initialize方法中,使用this._animFrames = makeAnimFrames(texture, frameWidth, frameHeight);从精灵表中获得坦克的四方行走动画序列的帧信息,在Sprite_Tank.prototype.updateCurrentFrame方法中每过一段固定时间就调用setFrame方法根据帧信息绘制精灵表中的相关帧图片到屏幕,最终形成坦克的行走动画(当然,现在还没有给坦克提供行走速度,所以是在原地的行走动画),如下图:

坦克的行走动画

玩家坦克使用的精灵表图片如下图(每帧图片大小为40x40)。
玩家坦克精灵表 TankPlayer.png

  坦克可以使用Sprite_Tank.prototype.look方法改变朝向,可以使用Sprite_Tank.prototype.move方法进行移动,可以使用Sprite_Tank.prototype.fire方法进行开火,开火有冷却效果,不能连续开火,开火后获得一个Sprite_Bullet对象,由战场场景Scene_TankWar负责它的移动、碰撞检测、移除等。当坦克的HP为0时,坦克进入“正在死亡”状态,经过一段指定的时间(这段时间主要是用于显示爆炸动画的,使得坦克被击中HP变0后爆炸动画显示完毕前还能显示在场景上)后进入“已经死亡”状态,然后战场场景Scene_TankWar就会将已经死亡的坦克从场景中移除。

Sprite_Enemy类解析

/**
 * 敌人坦克类,继承自坦克类,相对于坦克类,主要是添加了简单AI功能
 * @constructor
 */
function Sprite_Enemy() {
    this.initialize.apply(this, arguments);
};

Sprite_Enemy.prototype = Object.create(Sprite_Tank.prototype);
Sprite_Enemy.prototype.constructor = Sprite_Enemy;

/**
 * 改变坦克的前进方向
 */
Sprite_Enemy.prototype.changeRoute=function () {
    this._routeTick=0;
    this.look(Math.randomInt(4));
    this._desireRouteTick=Math.randomInt(40)+100;
};

Sprite_Enemy.prototype.initialize = function (texture, frameWidth, frameHeight, hp) {
    Sprite_Tank.prototype.initialize.apply(this, arguments);

    this.isStop=false;          //是否停止行动
    this._desireMoveTick=30;    //坦克的移动动画二帧间隔的时间
    this._desireFireTick=60;    //坦克开火后二次开火间的间隔时间
    this._desireRouteTick=100;  //坦克每次改变前进路线所需要的时间
    this._routeTick=0;          //从坦克上次改变前进路线开始流逝的时间
    this._desireDieTick = 40;   //坦克从死亡到已经死亡所需要的时间
}

Sprite_Enemy.prototype.update = function () {
    Sprite_Tank.prototype.update.call(this);

    if (this.state == Tank_State.Live && !this.isStop) { //如果坦克还活着,且没有要求停止行动(如果玩家被消灭,所有敌人坦克会被要求停止行动)
        //检测坦克是否碰上了场景的上、下、左、右边界,如果是,则自动转向(不论上次转向开始后流逝时间是否到达指定时间)
        if (this.x <= this.width / 2) {
            this.x = this.width / 2;
            this.changeRoute();
        }
        if (this.x >= Graphics.boxWidth - this.width / 2) {
            this.x = Graphics.boxWidth - this.width / 2;
            this.changeRoute();
        }
        if (this.y <= this.height / 2) {
            this.y = this.height / 2;
            this.changeRoute();
        }
        if (this.y >= Graphics.boxHeight - this.height / 2) {
            this.y = Graphics.boxHeight - this.height / 2;
            this.changeRoute();
        }

        //当上次转向后流逝的时间到达指定时间后开始转向
        this._routeTick++;
        if (this._routeTick >= this._desireRouteTick) this.changeRoute();
        this.move();
    }
};

Sprite_Enemy敌人坦克类,继承自坦克类Sprite_Tank,相对于坦克类,主要是在initialize方法重新调整了一些属性以及添加了自己特有的一些属性(AI功能需要使用);在update方法中添加了简单AI功能。该坦克在行驶随机时间后,使用Sprite_Enemy.prototype.changeRoute随机改变前进路线,在前进时自动开火,在遇到上下左右的边界时自动转向。

Scene_TankWar类解析

/**
 * 坦克大战游戏主场景:战场场景
 * @constructor
 */
function Scene_TankWar() {
    this.initialize.apply(this, arguments);
};

Scene_TankWar.prototype = Object.create(Scene_Base.prototype);
Scene_TankWar.prototype.constructor = Scene_TankWar;

Scene_TankWar.prototype.initialize = function() {
    Scene_Base.prototype.initialize.call(this);

    this._isGameOver = false;     //游戏是否结束:如果玩家被消灭,或玩家消灭了20辆敌从坦克则游戏结束
    this._maxEnemyCount = 20;     //打完20个胜利
    this._eliminatedEnemy = 0;    //当前消灭的敌人数量
    this._desireFinishTick = 120; //游戏结束(输或赢)后转到结束画面的时间
    this._finishTick = 0;         //从结束开始流逝的时间
    this._playerSpeed = 2;        //玩家坦克的移动速度
    this._playerBullets = [];     //保存所有由玩家坦克发出的炮弹精灵
    this._enemyTanks = [];        //保存所有生成的敌人坦克精灵
    this._enemyBullets = [];      //保存所有由敌人坦克发出的炮弹精灵
    this._explodes = [];          //保存所有生成的爆炸效果精灵
};

/**
 * 加载精灵的纹理图片
 */
Scene_TankWar.prototype.loadTextures = function () {
    this._playerTexture=ImageManager.loadTankwar("TankPlayer");     //加载玩家坦克的纹理图片
    this._enemyTexture=ImageManager.loadTankwar("TankEnemy");       //加载敌人坦克的纹理图片
    this._bulletRedTexture=ImageManager.loadTankwar("BulletRed");   //加载炮弹的纹理图片
    this._explodeTexture=ImageManager.loadTankwar("Explode");       //加载爆炸效果的纹理图片
};

/**
 * 在场景上部y坐标为60的地方随机x位置生成敌人
 */
Scene_TankWar.prototype.createEnemy = function () {
    var tankEnemy = new Sprite_Enemy(this._enemyTexture, 40, 40, 1); //敌人坦克使用精灵表TankEnemy.png,其尺寸160x160,每行4帧图片,每列4帧图片,所以每帧的宽高为40x40
    tankEnemy.speed = 2;//设置坦克的行驶速度
    tankEnemy.x = 60 + Math.randomInt(Graphics.boxWidth - 120); //设置坦克随机x坐标,这样使每个敌人坦克出生地都不一样
    tankEnemy.y = 60;   //坦克出生的y坐标始终在60处
    tankEnemy.look(Direction.Down); //初始时让敌人坦克面向下
    this.addChild(tankEnemy);       //将坦克加入到场景

    //使用MV中的动画来展示敌人坦克出现时的一个发光传送效果
    var animation = $dataAnimations[46];            //根据动画ID获取MV数据库中的动画
    tankEnemy.startAnimation(animation, false, 0);  //让坦克展示此动画(动画会跟着坦克走)
    this._enemyTanks.push(tankEnemy);               //将敌人坦克对象加入_enemyTanks数组中,以便于后续操作
};


/**
 * 在指定位置生成爆炸精灵
 * @param x 爆炸精灵要显示在的x坐标
 * @param y 爆炸精灵要显示在的y坐标
 */
Scene_TankWar.prototype.createExplode = function (x, y) {
    var explode = new Sprite_Explode(this._explodeTexture); //图片Explode.png由8帧组成,只有1行,尺寸为1024128
    explode.x = x;
    explode.y = y;
    explode.anchor = new Point(0.5, 0.5);
    explode.scale = new Point(0.7, 0.7); //由于素材比较大,所以可以用scale来缩小精灵到原来的0.7倍
    this._explodes.push(explode); //将爆炸对象加入_explodes数组中,以便于后续操作
    this.addChild(explode);
};

Scene_TankWar.prototype.create = function () {
    Scene_Base.prototype.create.call(this);

    this._backgroundSprite = new Sprite(ImageManager.loadTankwar("Background"));//创建背景精灵用于显示背景Background.png图片
    this.addChild(this._backgroundSprite); //将背景精灵加入场景
    this.loadTextures(); //加载所需的素材
};

Scene_TankWar.prototype.start = function () {
    Scene_Base.prototype.start.call(this);

    //播放开始音效 TankWarStart.ogg / TankWarStart.m4a,注意参数格式是一个包含特定属性的对象
    AudioManager.playSe({
        name:"TankWarStart",    //音频文件名
        pan:0,                  //pan值,可能是用于声道均衡的值,参考:https://en.wikipedia.org/wiki/Panning_%28audio%29
        pitch:100,              //pitch音高值
        volume:100              //volume音量值
    });
    //增加玩家坦克到场景中
    this._player=new Sprite_Tank(this._playerTexture, 40, 40, 2);//玩家坦克使用精灵表TankPlayer.png,其尺寸160x160,每行4帧图片,每列4帧图片,所以每帧的宽高为40x40
    this._player.speed=0; //坦克的初始速度为0,因为这个坦克是由玩家操控的,一开始玩家未操控时速度就是0,静止的
    this._player.x=Graphics.boxWidth/2; //将坦克的x坐标设置在场景的正中间
    this._player.y=Graphics.height-this._player.height-20; //坦克的y坐标设置在场景的底部向上20个单位处
    this.addChild(this._player); //将坦克加入场景
};

Scene_TankWar.prototype.update = function () {
    Scene_Base.prototype.update.call(this);

    //按键检测和处理
    if (this._player.state == Tank_State.Live) {
        this._player.speed = 0; //先取消速度,因为玩家可能没有按任何方向键
        if (Input.isPressed("down")) {              //按向下键
            this._player.look(Direction.Down);      //让坦克面朝下
            this._player.speed = this._playerSpeed; //重置速度
        }
        if (Input.isPressed("left")) {              //按向左键
            this._player.look(Direction.Left);      //让坦克面朝左
            this._player.speed = this._playerSpeed;
        }
        if (Input.isPressed("right")) {             //按向右键
            this._player.look(Direction.Right);     //让坦克面朝右
            this._player.speed = this._playerSpeed;
        }
        if (Input.isPressed("up")) {                //按向上键
            this._player.look(Direction.Up);        //让坦克面朝上
            this._player.speed = this._playerSpeed;
        }
        if (Input.isPressed("control") && this._player.canFire) {   //按Ctrl键发射炮弹
            var bullet = this._player.fire(this._bulletRedTexture); //玩家坦克开火,得到炮弹对象
            this._playerBullets.push(bullet);   //将玩家打出的炮弹加入_playerBullets数组中,以便于后续操作
            this.addChild(bullet);              //将炮弹加入到场景中
        }
        if (this._player.speed != 0) this._player.move();//移动玩家坦克
    }

    //玩家打出的炮弹出界检测,如果炮弹超出画面边界,则将它们从游戏中移除
    for (var i = this._playerBullets.length - 1; i >= 0; i--) {
        this._playerBullets[i].move();
        if (this._playerBullets[i].x >= Graphics.boxWidth ||
            this._playerBullets[i].x <= 0 ||
            this._playerBullets[i].y >= Graphics.boxHeight ||
            this._playerBullets[i].y <= 0) {
            var outBullet = this._playerBullets.splice(i, 1)[0]; //找到一个出界的炮弹
            this.removeChild(outBullet); //从画面移除出界的炮弹
        }
    }
    //玩家炮弹与敌人碰撞检测,如果炮弹与敌人坦克碰撞,炮弹消失,敌人受到1点伤害
    for (var i = this._playerBullets.length - 1; i >= 0; i--) {
        for (var ti = this._enemyTanks.length - 1; ti >= 0; ti--) {
            if (this._enemyTanks[ti].state != Tank_State.Live) continue; //正在死亡或已经死亡的就不用处理了,也就是炮弹能穿过它们
            if (this._playerBullets[i].x >= this._enemyTanks[ti].x - this._enemyTanks[ti].width / 2 &&
                this._playerBullets[i].x <= this._enemyTanks[ti].x + this._enemyTanks[ti].width / 2 &&
                this._playerBullets[i].y >= this._enemyTanks[ti].y - this._enemyTanks[ti].height / 2 &&
                this._playerBullets[i].y <= this._enemyTanks[ti].y + this._enemyTanks[ti].height / 2) {

                var deadBullet = this._playerBullets.splice(i, 1)[0];//找到一个与敌人坦克碰撞的炮弹
                this.removeChild(deadBullet); //将炮弹从场景中移除
                this._enemyTanks[ti].hurt(1);       //被炮弹击中的敌人坦克受到1点HP伤害
                if (this._enemyTanks[ti].hp <= 0) { //检测敌人坦克是否还有hp生命值,如果死亡:
                    this._eliminatedEnemy++;        //玩家消灭的敌人数量增加1
                    this._isGameOver = this._eliminatedEnemy >= this._maxEnemyCount;    //如果消灭的敌人数量达到20个,游戏结束
                    this.createExplode(this._enemyTanks[ti].x, this._enemyTanks[ti].y); //在坦克的位置显示一个爆炸效果
                    AudioManager.playSe({   //播放一个爆炸音效 Explosion1.ogg / Explosion1.m4a
                        name: "Explosion1",
                        pan: 0,
                        pitch: 100,
                        volume: 100
                    });
                }
                break;
            }
        }
    }
    //检测是否有死亡的坦克,将其从场景内移除
    for (var i = this._enemyTanks.length - 1; i >= 0; i--) {
        if (this._enemyTanks[i].state == Tank_State.Dead) { //依次检测每个坦克的状态,看是否死亡
            var deadTank = this._enemyTanks.splice(i, 1)[0]; //找到一辆死亡的坦克
            this.removeChild(deadTank); //将死亡的坦克从战场移除
        }
    }

    //创建新的敌人加入战场
    if (!this._isGameOver && //未结束游戏时才允许增加敌人
        this._eliminatedEnemy + this._enemyTanks.length < this._maxEnemyCount && //被消灭的敌人数量和在场上的敌人数量不足最大值(20辆)时才允许增加敌人
        this._enemyTanks.length < 4) { //场上的敌人不足4人时才允许增加敌人
        this.createEnemy(); //创建新的敌人并将它加入战场
    }
    //敌人坦克自动开火
    if (!this._isGameOver) {//如果游戏未结束才允许敌人开火
        for (var i in this._enemyTanks) {
            if (this._enemyTanks[i].canFire) { //检测坦克是否能开火
                var bullet = this._enemyTanks[i].fire(this._bulletRedTexture); //坦克开火,生成一个炮弹对象
                this._enemyBullets.push(bullet); //将炮弹对象加入_enemyBullets数组,以便于后续操作
                this.addChild(bullet); //将炮弹加入场景
            }
        }
    }
    //敌人炮弹出界检测,如果炮弹超出画面边界,则将它们从游戏中移除
    for (var i = this._enemyBullets.length - 1; i >= 0; i--) {
        this._enemyBullets[i].move();
        if (this._enemyBullets[i].x >= Graphics.boxWidth ||
            this._enemyBullets[i].x <= 0 ||
            this._enemyBullets[i].y >= Graphics.boxHeight ||
            this._enemyBullets[i].y <= 0) {
            var outBullet = this._enemyBullets.splice(i, 1)[0]; //找到一个出界的炮弹
            this.removeChild(outBullet); //从画面移除出界的炮弹
        }
    }
    //敌人炮弹与玩家碰撞检测,如果敌人炮弹碰到玩家坦克,炮弹消失,玩家受1点伤害
    for (var i = this._enemyBullets.length - 1; i >= 0; i--) {
        if (this._enemyBullets[i].x >= this._player.x - this._player.width / 2 &&
            this._enemyBullets[i].x <= this._player.x + this._player.width / 2 &&
            this._enemyBullets[i].y >= this._player.y - this._player.height / 2 &&
            this._enemyBullets[i].y <= this._player.y + this._player.height / 2) {

            var deadBullet = this._enemyBullets.splice(i, 1)[0];//找到一个与玩家坦克碰撞的炮弹
            this.removeChild(deadBullet); //将炮弹从场景中移除
            this._player.hurt(1);         //玩家受到1点HP伤害
            if (this._player.hp <= 0) {   //检测玩家是否还有hp生命值,如果死亡:
                this.createExplode(this._player.x, this._player.y); //创建一个爆炸效果
                AudioManager.playSe({     //播放一个爆炸音效 Explosion1.ogg / Explosion1.m4a
                    name: "Explosion1",
                    pan: 0,
                    pitch: 100,
                    volume: 100
                });
            } else { //如果玩家坦克还有hp生命值
                AudioManager.playSe({ //播放一个被打击的音效 Shot2.ogg / Shot2.m4a
                    name: "Shot2",
                    pan: 0,
                    pitch: 100,
                    volume: 100
                });
            }
            break;
        }
    }

    //检测玩家坦克是否死亡,如果死亡游戏结束
    if (!this._isGameOver && this._player.state == Tank_State.Dead) {//如果玩家死亡:
        this.removeChild(this._player); //将玩家坦克从场景移除
        AudioManager.playSe({   //播放失败的音效 TankWarLost.ogg / TankWarLost.m4a
            name: "TankWarLost",
            pan: 0,
            pitch: 100,
            volume: 100
        });
        this._isGameOver = true; //将游戏结束设置为true
        for (var i in this._enemyTanks) {//停止所有敌人坦克的行动
            this._enemyTanks[i].isStop = true;
        }
    }

    //爆炸动画更新:爆炸动画有8帧组成,如果爆炸的动画播放完毕就将它们从场景中移除
    for (var i = this._explodes.length - 1; i >= 0; i--) {
        if (this._explodes[i].isFinished) {
            var explode = this._explodes.splice(i, 1)[0];
            this.removeChild(explode);
        }
    }

    //游戏结束检测
    if (this._isGameOver) {
        this._finishTick++;
        if (this._finishTick >= this._desireFinishTick) { //当流逝时间达到结束游戏需要等待的时间,则转场到游戏结束场景
            var isWin = this._player.state == Tank_State.Live; //如果玩家还活着就算胜利(其实这里还有个隐藏条件,就是玩家消灭了20个敌人坦克,因为游戏结束只有二种可能,一是玩家坦克被消灭,二是玩家消灭20辆敌人坦克,所以这里不用再检测该条件)
            SceneManager.push(Scene_TankWarGameOver);     //准备转场到游戏结束场景
            SceneManager.prepareNextScene(isWin);         //向游戏结束场景传递参数:玩家是否赢了游戏
        }
    }
};

这个类对应于战场场景,是本游戏的核心类,负责管理游戏的各项功能。首先在initialize方法中设定初始属性、变量。在Scene_TankWar.prototype.create方法中添加场景背景和初始化图片资源。在Scene_TankWar.prototype.start方法中,也就是场景刚开始时播放一段坦克大战的经典音乐片段,并将玩家坦克加入到场景的正中心下部。在Scene_TankWar.prototype.update方法中,首先处理玩家对玩家坦克的控制,包括转向、开火、前进等;然后检查玩家打出的炮弹是否超出边界(超出就从场景中删除),是否与敌人坦克发生碰撞(伤害或击毁敌人坦克),有碰撞时如果击毁了敌人则显示一个爆炸效果;检查是否有“已经死亡”的坦克,将它们从场景移除(坦克被击中后HP变为0时此时进入“正在死亡”状态,正在死亡时坦克无法行动、开火,经过一段设定的时间后,最终变成“已经死亡”状态,就可以移除了)。 根据当前场上的敌人数量,如果不足4辆坦克,则使用createEnemy增加新的敌人坦克,敌人坦克出生时会调用startAnimation来运行一个MV动画来造势;同时检查敌人炮弹是否超出边界,是否与玩家坦克发生碰撞(伤害或击毁玩家坦克)。如果玩家坦克被击毁,则播放一段游戏失败的音乐片段,经过一段指定时间后转场到结束场景Scene_TankWarGameOver,并使用SceneManager.prepareNextScene(false);向其传递false参数表示“输了”;反过来,如果玩家消灭了20辆坦克,同样也进入结束场景Scene_TankWarGameOver,但传递true的参数表示“赢了”。

Scene_TankWarGameOver类解析

/**
 * 坦克大战游戏结束画面场景
 * @constructor
 */
function Scene_TankWarGameOver() {
    this.initialize.apply(this, arguments);
};

Scene_TankWarGameOver.prototype = Object.create(Scene_Base.prototype);
Scene_TankWarGameOver.prototype.constructor = Scene_TankWarGameOver;

/**
 * 用于本场景接收传递来的参数
 * @param isWin 是否取得胜利
 */
Scene_TankWarGameOver.prototype.prepare = function(isWin) {
    this._isWin = isWin;
};

Scene_TankWarGameOver.prototype.create = function () {
    Scene_Base.prototype.create.call(this);

    this._backgroundSprite=new Sprite(ImageManager.loadTankwar("TitleBack"));//显示背景图片的精灵
    this.addChild(this._backgroundSprite);

    var image = ImageManager.loadTankwar(this._isWin ? "YouWin" : "YouLose");//根据输赢加载相应的图片
    this._logo=new Sprite(image);//显示输赢logo的精灵
    this._logo.anchor=new Point(0.5,0.5);
    this._logo.x=Graphics.boxWidth/2;
    this._logo.y=Graphics.boxHeight/2;
    this.addChild(this._logo);
};

Scene_TankWarGameOver.prototype.update = function () {
    if(Input.isTriggered('ok') || TouchInput.isTriggered()){
        SceneManager.goto(Scene_TankWarTitle);//进入标题画面场景
    }
};

这个是结束场景类,很简单,在create方法中增加背景精灵,并定义一个prepare 方法,用于接收由战场场景传过来的代表游戏“输赢”的参数,根据输赢结果显示“你赢了”或“你输了”的图片文本。在update方法中,检测用户是否按了确定键或点击了屏幕,如果是,则重新回到标题场景Scene_TankWarTitle
  为什么这里定义prepare方法就能接收来自战场场景Scene_TankWar的参数呢?这个涉及SceneManager.prepareNextScene 方法的实现,因为我们在转场时会调用该方法传递参数,而该方法的实现方法如下面的代码所示,它实际是调用_nextSceneprepare方法来传递参数的,_nextScene就是要转场去的新场景(这里就是指游戏结束场景Scene_TankWarGameOver),所以我们只需要在游戏结束场景中定义好prepare 方法就可以接收来自战场场景的参数了。

SceneManager.prepareNextScene = function() {
    this._nextScene.prepare.apply(this._nextScene, arguments);
};

本文解析的比较简单,一开始打算把如何构建整个代码的过程全部写出来,但写了1/3发现这文章实在会变得太长太长,或许录制成视频教程会更好。最后决定只将基础部知识分先提出来,再简单解析一下各个类的实现,最重要的是在源代码中详细标注了一下各行代码的用途。这个系列需要你知道js的基本编程知识,所以如果没学过js,或者没有oop概念的话,可能看不明白,建议先啃一下js编程的书籍。

本文没有涉及到障碍物及其击毁、阻碍,也没有去实现敌我的子弹碰撞时一起销毁等,还有二辆坦克碰撞时也没有防止它们互相叠加,不过,这此功能主要是碰撞检测,可以参考本文中的炮弹与坦克碰撞检测来实现。本文还有下部,下部中主要是增加一些界面元素来显示相关参数,比如显示敌人数量、消灭的敌人数量、血槽等。

by Mandarava(鳗驼螺) 2017.06.27

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容