欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

使用PixiJS做一个小游戏

程序员文章站 2022-05-10 13:09:18
PixiJS PixiJS使用WebGL,是一个超快的HTML5 2D渲染引擎。作为一个Javascript的2D渲染器,Pixi.js的目标是提供一个快速的、轻量级而且是兼任所有设备的2D库。 官方网址: http://www.pixijs.com/ 知识点 做一个小游戏,我们使用到PixiJS的 ......

pixijs

pixijs使用webgl,是一个超快的html5 2d渲染引擎。作为一个javascript的2d渲染器,pixi.js的目标是提供一个快速的、轻量级而且是兼任所有设备的2d库。

官方网址: 

知识点

做一个小游戏,我们使用到pixijs的功能不多,只需要了解以下几个点即可快速上手。

  • pixi.application 创建一个游戏时第一个要初始化的对象。
  • stage 舞台,我们可以看做是所有对象的根节点,类似于document。
  • pixi.loader 资源加载和管理器。
  • pixi.texture 材质,通常是指我们加载的图片。
  • pixi.sprite 精灵,就是游戏中的一个对象,结合pixi.texture 材质使用。
  • pixi.extras.animatedsprite 动画精灵,可以设置多个图片,按序播放。
  • pixi.container 精灵容器,我们可以把多个精灵结合在一起组成一个更复杂的对象。

了解以上内容我们就可以直接做小游戏了,其它知识可以去官网查看。

游戏制作

此为一个躲避下落物体的小游戏,体验地址 (移动端):

初始化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函数,都在这里自已更新自已的位置和状态(updateapp.ticker定时调用)。所有对外开放的状态设置都提供接口,比如diemove等。 如下:

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. 一阶段比较简单,每排放置1和2个,并且行距比较大,掉落速度最慢。
  2. 二阶段每排放1和3个,增加一定速度。
  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;
};

  

此小游戏主要内容就这么多,具体的可以细看代码: