- 作者:Mandarava(鰻駝螺)
- 微博:@鰻駝螺pro
MV中的小游戲可以用事件來做,也可以用插件來寫。本文將使用插件的方式編寫一個類似于坦克大戰的游戲。本文的目的并非展示如何制作一個完整的坦克大戰游戲,而是學習如何使用MV的插件來編寫小游戲。所以本文的重點是介紹制作MV小游戲可能用到的相關方法,希望拋磚引玉,參考本文的實現方式,能讓各位編寫出各種類型的其它小游戲。
本文所涉及的內容:
- 游戲結構及流程介紹
- 相關素材資源的下載和使用
- 基礎知識:音效的播放
- 基礎知識:精靈表的切幀
- 基礎知識:使用MV中的動畫
- Scene_TankWarTitle類解析
- Sprite_Bullet類解析
- Sprite_Explode類解析
- Sprite_Tank類解析
- Sprite_Enemy類解析
- Scene_TankWar類解析
- Scene_TankWarGameOver類解析
游戲結構及流程介紹
整個坦克大戰游戲包含三個場景:標題場景(Scene_TankWarTitle
類)、戰場場景(Scene_TankWar
類)、結束場景(Scene_TankWarGameOver
類)。在標題場景,按確定鍵或點擊畫面將進入戰場場景,展開坦克對戰。在戰場場景玩家坦克出生在場景中間靠底部的位置(在MV的場景中,畫面的左上角為坐標原點),敵人在頂部的隨機位置出生,出生時帶有類似傳送效果的動畫(該動畫來自MV數據庫中的自帶動畫)。同屏最多顯示4輛敵人坦克,每消滅一輛就會隨即出生一輛新的,整場戰斗最多出生20輛敵人坦克。敵人坦克擁有簡單的AI,可以隨機移動、開火。默認敵人坦克每輛只有1HP生命值,玩家坦克則為2HP;坦克每次被炮彈擊中將損傷1HP生命值。當玩家消滅所有20輛坦克,或被坦克消滅后,將進入結束場景,根據玩家的輸贏,顯示“你輸了”或“你贏了”的圖片文字。效果如下:
為了開發與調試的快捷,可以重寫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
并不是單純的音頻文件名稱,而是一個指定了特定屬性的對象,這個對象中需要指定音頻文件名name
,pan
(這可能是有關聲道均衡的值,-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。
該圖片上第一排是坦克向下運動時的行走動畫幀,第二排是向左運動時,第三排是向右運動時,第四排是向上運動時。
從這張圖片很容易知道每個幀圖片在這張大圖上的位置和高寬尺寸。首先,各幀圖片的高寬尺寸前面說了都是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
是精靈圖片,frameWidth
和frameHeight
表示幀圖片的寬高尺寸。那么要對上面這張玩家坦克精靈表切幀就可以使用下面的方法:
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
并不是最終目的,我們的最終目的是通過在更新方法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
就會知道這個爆炸效果已經結束,可以把它刪除了。
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)。
坦克可以使用
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
方法的實現,因為我們在轉場時會調用該方法傳遞參數,而該方法的實現方法如下面的代碼所示,它實際是調用_nextScene
的prepare
方法來傳遞參數的,_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