PixiJS
PixiJS使用WebGL,是一個超快的HTML5 2D渲染引擎。作為一個Javascript的2D渲染器,Pixi.js的目標是提供一個快速的、輕量級而且是兼任所有設備的2D庫。
官方網址: http://www.pixijs.com/
知識點
做一個小游戲,我們使用到PixiJS的功能不多,只需要了解以下幾個點即可快速上手。
PIXI.Application創建一個游戲時第一個要初始化的對象。stage舞台,我們可以看做是所有對象的根節點,類似於document。PIXI.loader資源加載和管理器。PIXI.Texture材質,通常是指我們加載的圖片。PIXI.Sprite精靈,就是游戲中的一個對象,結合PIXI.Texture材質使用。PIXI.extras.AnimatedSprite動畫精靈,可以設置多個圖片,按序播放。PIXI.Container精靈容器,我們可以把多個精靈結合在一起組成一個更復雜的對象。
了解以上內容我們就可以直接做小游戲了,其它知識可以去官網查看。
游戲制作
此為一個躲避下落物體的小游戲,體驗地址 (移動端):https://jiamao.github.io/pixigame/game.html
初始化PixiJS
var opt = {
width: window.innerWidth,
height: window.innerHeight,
antialias: true, // default: false
transparent: false, // default: false
resolution: 1 // default: 1
};
//生成app對象,指定寬高,這里直接全屏
var app = new PIXI.Application(opt);
app.renderer.backgroundColor = 0xffffff;
app.renderer.autoResize = true;
//這里使用app生成的app.view(canvas)
document.body.appendChild(app.view);
//這里是APP的ticker,會不斷調用此回調
//我們在這里去調用游戲的狀態更新函數
app.ticker.add(function(delta) {
//理論上要用delta去做時間狀態處理,我們這里比較簡單就不去處理時間問題了
//每次執行都當做一個有效的更新
game.update(delta);
});
資源加載
加載資源使用PIXI.loader,支持單個圖片,或雪碧圖的配置json文件。
PIXI.loader .add(name1, 'img/bg_1-min.jpg') .add(name2, 'img/love.json').load(function(){ //加載完 });
雪碧圖和其json配置文件可以用工具TexturePackerGUI來生成, 格式如下:
{"frames": {
"bomb.png":
{
"frame": {"x":0,"y":240,"w":192,"h":192},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":192,"h":192},
"sourceSize": {"w":192,"h":192}
},
...//省略多個
"x.png":
{
"frame": {"x":576,"y":240,"w":192,"h":192},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":192,"h":192},
"sourceSize": {"w":192,"h":192}
}},
"animations": {
"m": ["m1.png","m2.png"]
},
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "1.0",
"image": "love.png?201902132001",
"format": "RGBA8888",
"size": {"w":768,"h":432},
"scale": "1",
"smartupdate": "$TexturePacker:SmartUpdate:5bb8625ec2f5c0ee2a84ed4f5a6ad212:f3955dc7846d47f763b8c969f5e7bed3:7f84f9b657b57037d77ff46252171049$"
}
}
精靈
加載完資源后,我們就可以用PIXI.loader.resources讀取資源,制作一個普通精靈。
var textures = PIXI.loader.resources['qq'].textures; var sprite = new PIXI.Sprite(textures['qq_head.png']);
動畫
跟上面普通精靈類似,只是使用多個圖片做為偵。然后用PIXI.extras.AnimatedSprite來播放。 例如下面我們取雪碧圖中f開頭的圖片組成一個動畫。
var textures = PIXI.loader.resources['bling'];
var expTextures = [];//當前動畫所有材質集合
var keys = textures.data.animations['f'];
//按索引排個序,以免偵次序亂了
keys.sort(function(k1,k2){
return k1.replace(/[^\d]/g,'') - k2.replace(/[^\d]/g,'');
});
for(var i=0;i<keys.length;i++) {
var t = textures[keys[i]];
expTextures.push(t);
}
var side = new PIXI.extras.AnimatedSprite(expTextures);
side.animationSpeed = 0.15;//指定其播放速度
app.stage.addChild(side);
//其它接口請查看官方文檔
狀態更新
每個對象都有一個update函數,都在這里自已更新自已的位置和狀態(update由app.ticker定時調用)。所有對外開放的狀態設置都提供接口,比如die、move等。 如下:
this.die = function() {
this.state = 'dead';
this.sprite.visible = false;
map.removeBob(this);
}
//發生碰撞,炸彈會導致氣球破裂
this.hitEnd = function() {
//氣球破裂
heart.break(function(){
console.log('我跟氣球撞了');
});
}
//更新炸彈狀態
this.update = function(delta) {
//計算當前在屏幕中的坐標
var p = map.toLocalPosition(this.position.x, this.position.y);
//運行中,障礙物到屏幕時才需要顯示
if(game.state == 'play' && p.y >= -this.sprite.height) {
this.start();
}
if(!this.sprite.visible) return;
//移動精靈
this.sprite.x = p.x;
this.sprite.y = p.y;
//出了屏外,則不需要再顯示
if(p.y > game.app.screen.height) {
this.die();
return;
}
//如果碰到當前精靈,則精靈死
if(heart.hitTest(this)) {
this.hitEnd();
}
this.position.y += this.vy; //保持自身的速度
}
游戲設計
地圖
背景
游戲的背景是一張超長的圖:
- 第一要考慮的就是分辨率問題,因為高度相對於屏來說是夠長的,這里我們以寬度跟屏寬的比來做為縮放比例,而且所有游戲元素都是相對於背景設計的,因此所有元素都采用此縮放比即可。 此處代碼都是在游戲map對象中的。
this.background = new pixiContainer(); //地圖元素的container this.scale = (this.width / this.bg_width).toFixed(4) * 1;//地圖寬縮放比例,為整個地圖縮放比例 this.height = this.bg_height * this.scale; this.background.scale.x = this.scale; this.background.scale.y = this.scale;
計算對象在屏幕中的坐標
//轉為畫布坐標
toLocalPosition: function(x, y) {
if(typeof x == 'object') {
y = x.y;
x = x.x;
}
x = x||0;
y = y||0;
//x坐標為地圖偏移量在對象在地圖的坐標
x = x + this.offsetPosition.x;
//y為屏高+當前地圖相對屏的偏移量,加上對象在地圖的Y坐標再減去屏幕高度。
y = y + game.app.screen.height + this.offsetPosition.y - this.height;
return {
x: x,
y: y
};
},
- 圖片加載問題,如果直接加載長圖效率太低。我們把圖切成等高的五份。首次加載最底下的圖,其它位置只用一個空精靈占位,再異步加載其它四張后替換其材質即可。
//初始化背景圖
var bgspOffsetY = 0;
var bgHeights = [1646,1640,1652,1652,1637];
//默認只加載了第一張圖,其它的全用第一張圖占位先,加載完后再覆蓋
for(var i=bgHeights.length-1; i>=0; i--) {
var bgsp = new pixiSprite(pixiResources['map_background1'].texture);
bgsp.position.set(0, bgspOffsetY);
this.background.addChild(bgsp);
bgspOffsetY += bgHeights[i];
}
//load其它背景圖
loadBackground: function(hs) {
var bg2=loadSource('map_background2', cdnDomain+'img/bg_2-min.jpg');
var bg3=loadSource('map_background3', cdnDomain+'img/bg_3-min.jpg');
var bg4=loadSource('map_background4', cdnDomain+'img/bg_4-min.jpg');
var bg5=loadSource('map_background5', cdnDomain+'img/bg_5-min.jpg');
if(!bg2.isComplete||!bg3.isComplete||!bg4.isComplete||!bg5.isComplete){
pixiLoader.load(function(){
//children中,第一張是第五張,依次
for(var i=5;i>1;i--) {
map.background.children[5-i].texture = pixiResources['map_background' + i].texture;
}
});
}
else {
for(var i=5;i>1;i--) {
this.background.children[5-i].texture = pixiResources['map_background' + i].texture;
}
}
}
- 背景、障礙物和氣球滑動問題。解決這個問題,我們把所有地圖上的物體都初始化在背景上,它們的位置都是相對於背景的。當執行update時,實時根據地圖相對於屏幕的位置來更新對象在屏幕上的坐標。
氣球
氣球跟所有物體一樣,有多個狀態,當吃糖時還會有相應的動畫。 比如,氣球在復活時有一定時間的無敵狀態,這時我們就要一閃一閃來表示。
updateGoldAni: function() { //無敵顯示狀態 ,只隱顯幾下即可 if(this.state == 'gold') { if(this.container.alpha >= 1) { this.__appha_dir = 0; } else if(this.container.alpha <= 0.4) { this.__appha_dir = 1; } if(this.__appha_dir) { this.container.alpha += 0.02; } else { this.container.alpha -= 0.02; } } else if(this.container.alpha != 1) { this.container.alpha = 1; } },
滑動事件
由於無論滑到屏幕任何位置都需要有效,則把事件綁到stage上。PixiJS對象如果要響應事件,則必須把interactive設置為true。
//綁定滑動事件 bindEvent: function() { var isTouching = false; //是否在移動中 var lastPosition = {x:0, y:0};//最近一次移到的地方 this.app.stage.interactive = true; this.app.stage.on('touchstart', function(e){ if(game.state == 'play') { isTouching = true; lastPosition.x = e.data.global.x; lastPosition.y = e.data.global.y; e.data.originalEvent && e.data.originalEvent.preventDefault && e.data.originalEvent.preventDefault(); //console.log(e.data.global) } }).on('touchmove', function(e){ if(isTouching && game.state == 'play') { //console.log(e.data.global, lastPosition); var offx = e.data.global.x - lastPosition.x; heart.move(offx); //移動氣球,只讓橫向移動 lastPosition.x = e.data.global.x; lastPosition.y = e.data.global.y; } e.data.originalEvent && e.data.originalEvent.preventDefault && e.data.originalEvent.preventDefault(); }).on('touchend', touchEnd).on('touchcancel', touchEnd).on('touchendoutside', touchEnd); function touchEnd(e) { heart.m_state = 'normal'; console.log('normal') isTouching = false; heart.line.gotoAndStop(0); e.data.originalEvent && e.data.originalEvent.preventDefault && e.data.originalEvent.preventDefault(); } },
在移動時,需要播放氣球線條的左右移動畫。line是一個animation精靈。
var newx = this.container.x + offsetX; var directX = newx - this.container.x; //往右移動 if(directX > 0) { if(this.m_state != 'right') { //開始右移動畫 this.line.gotoAndPlay(1); } this.m_state = 'right'; //往右移動 } //往左移動 else if(directX < 0) { if(this.m_state != 'left') { //開始右移動畫 this.line.gotoAndPlay(5); } this.m_state = 'left'; //往左移動 } //超過一定時間沒移動,則回到正常位置 this.__moveTimeHandler = setTimeout(function(){ heart.m_state = 'normal'; heart.line.gotoAndStop(0); }, 500); this.container.x = newx;
障礙物
障礙物和糖果只需要相對於地圖移動即可,為了保證路不被卡死,我們一排最多放置3個障礙物。 且難易分為三個階段
- 一階段比較簡單,每排放置1和2個,並且行距比較大,掉落速度最慢。
- 二階段每排放1和3個,增加一定速度。
- 三階段每排2和3個,速度最快,且行距最小。
為了游戲效果,上一行的的空檔和當前行空檔之前的物體上下浮動增加一些錯亂感。
碰撞檢測
這塊比較簡單,都是規則的矩形。
//二個矩形是否有碰撞
function hitTestRectangle(r1, r2) {
var hitFlag, combinedHalfWidths, combinedHalfHeights, vx, vy, x1, y1, x2, y2, width1, height1, width2, height2;
hitFlag = false;
x1 = r1.x;
x2 = r2.x;
y1 = r1.y;
y2 = r2.y;
width1 = r1.width;
width2 = r2.width;
height1 = r1.height;
height2 = r2.height;
//如果對象有指定碰撞區域,則我們采用指定的坐標計算
if(r1.hitArea) {
x1 += r1.hitArea.x * map.scale;
y1 += r1.hitArea.y * map.scale;
width1 = r1.hitArea.width * map.scale;
height1 = r1.hitArea.height * map.scale;
}
if(r2.hitArea) {
x2 += r2.hitArea.x * map.scale;
y2 += r2.hitArea.y * map.scale;
width2 = r2.hitArea.width * map.scale;
height2 = r2.hitArea.height * map.scale;
}
//中心坐標點
r1.centerX = x1 + width1 / 2;
r1.centerY = y1 + height1 / 2;
r2.centerX = x2 + width2 / 2;
r2.centerY = y2 + height2 / 2;
//半寬高
r1.halfWidth = width1 / 2;
r1.halfHeight = height1 / 2;
r2.halfWidth = width2 / 2;
r2.halfHeight = height2 / 2;
//中心點的X和Y偏移值
vx = r1.centerX - r2.centerX;
vy = r1.centerY - r2.centerY;
//計算寬高一半的和
combinedHalfWidths = r1.halfWidth + r2.halfWidth;
combinedHalfHeights = r1.halfHeight + r2.halfHeight;
//如果中心X距離小於二者的一半寬和
if (Math.abs(vx) < combinedHalfWidths) {
//如果中心V偏移量也小於半高的和,則二者碰撞
if (Math.abs(vy) < combinedHalfHeights) {
hitFlag = true;
} else {
hitFlag = false;
}
} else {
hitFlag = false;
}
return hitFlag;
};
此小游戲主要內容就這么多,具體的可以細看代碼:





