基于Canvas的JS游戏引擎(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jlin991/article/details/61970971

介绍

这是一个非常精简的游戏引擎,它仅仅包含了一般游戏所必须拥有的功能。

游戏引擎

主要功能如下
1. 游戏循环
2. 绘制精灵
3. 基于时间运动
4. 碰撞检测
5. 帧速率更新
4. 暂停游戏
5. 事件处理
6. 图片加载

动画循环 /游戏循环

其实游戏循环就是依赖动画循环实现的。
window.requestAnimationFrame()
传统的是window.setTimeout()

核心: 只需在播放动画时持续更新并绘制就行了。 持续更新和重绘:动画循环。
它是所有动画的核心逻辑。

setInterval setTimeout的缺点

使用setInterval实现动画循环,只需要调用一次,而setTimeOut() 则需要持续调用。
他们的问题: 对于setTimeout()它要明确告诉浏览器下一次执行动画循环的时间。所以,每次调用它都要把下次执行动画循环的时间点计算出来。
它不提供精确计时机制,它们只是让程序能在某个大致时间点上运行代码的通用方法而已。
“强制规定时间间隔的下限”。 况且浏览器也是在这么做。

比如FIREFOX 允许最小时间间隔是10ms。 后续调用的最小间隔是5ms。 这也就是说如果你以3ms 为参数来调用setTimeout()方法,浏览器就会根据规则认定参数无效。

不应主动命令浏览器何时去绘制下一frame动画,这应该由浏览器告诉你

虽然setTimeout() setInterval() 时间间隔机制不精确,不过调用的时候,会主动告知绘制下一frame的时间。然而调用者并不知道下一frame动画最佳时机,你可能根本不了解浏览器绘制动画内部机制。 我们应该让浏览器在它觉得可以绘制下一frame动画时通知你。我们用requestAnimationFrame() 实现。

function animate(time){
requestAnimationFrame(animate);
}
function animate() { ...}

实现动画效果

   animate:function(time,that){
        //let self=this; 
        if(this.paused){
            //check if the game is still paused , in PAUSE_TIMEOUT. no need to check
            //more frequently

            setTimeout(()=>{
                window.requestNextAnimationFrame((time)=>{
                    //this.animate.call(this,time);
                    this.animate(time,that);
                }) ; 
            },this.PAUSE_TIMEOUT);
        }else{  //game is not paused      
             this.updateSprites(time); //Invoke sprite behavirus
            //call this method again when it's time for  the next animation frame
            window.requestNextAnimationFrame((time)=>{
                this.animate.call(this,time); 
            });
        }
    },    

这个就是游戏循环,每次requestNextAnimationFrame都会重绘 ,绘制的内容就是这个动画中会动的所有对象,包括精灵。

动画重绘的问题

动画重绘是非常重要的一个地方,实现动画不一定是借助requestAnimationFrame()
绘制动画有3种方法
1. 将所有内容擦除,并重新绘制(requestAnimationFrame)
2. 仅重绘内容发生变化的那部分区域
3. 从offscreen缓冲区中将内容变化的那部分背景图像复制到屏幕上
是否需要全部重绘?
有时全部重绘,反倒可以获得最佳性能。 如果背景很简单,而且你要绘制的运动物体也比较简单的话,那么将所有内容都擦掉并重新绘制是个好办法。

剪辑区域实现动画

function draw(){
    var numDisc,disc,il
    for(i=0;i<numDiscs;++i){
        drawXXXBackground(disc[i]);
    }
    for(i=0;i<numDiscs;i++){
        drawDisc(disc[i]);
    }
}

function drawDiscBackground(disc){
    context.save();
    context.beginPath();
    context.arc(disc.lastX,disc.lastY,disc.radius+1,0,Math.PI*2,false);
    eraseBackground();
    drawBackground();
    context.restore();
}

图块实现动画

基本思想是多开一个offcanvas,离屏canvas对象来避免每帧动画都要重绘整个背景
然后将离屏canvas绘制到屏幕上,主要的动画循环函数仍然是draw()
只是drawDiscBackground不同了

function drawDiscBackground(context,disc){
    var x=disc.lastX,
        y=disc.lastY,
        r=disc.radius,
        w=r*2,
        h=r*2;
    context.save();
    context.beginPath();
    context.clip()
    context.arc(disc.lastX,disc.lastY,disc.radius+1,0,Math.PI*2,false);
    eraseBackground();
    drawBackground();
    context.restore();
}

绘制精灵

精灵

我们把一些基本功能封装为JS对象,我们赋予精灵各种行为
精灵 就是集成到动画之中的图形对象。

一个精灵的定义如下:

var Sprite=function(name,painter,behaviors){
    if(name!==undefined) this.name=name;
    if(painter!==undefined) this.painter=painter;
    if(behaviors!==undefined) this.behaviors=behaviors;

    return this;
};

//Prototype
Sprite.prototype={
    top:0, //全部都是默认值,不同的实例拥有同样的属性值  
    left:0,
    width:10,
    height: 10,
    velocityX: 0,
    velocityY: 0,
    fps:60,
    trap:false,
    freeze:false,
    tapTimes:0,
    color:[],
   visible: true,
   animating: false,
   painter: undefined, // object with paint(sprite, context)
   behaviors: [], // objects with execute(sprite, context, time)
   //绘制方法
    paint:function(context){
        if(this.painter!==undefined&&this.visible){
            this.painter.paint(this,context);
        }
    },
    //update执行所有精灵行为
    update:function(context,time){
        for(var i=0;i<this.behaviors.length;++i){
            this.behaviors[i].execute(this,context,time);
        }
    }
}

这个精灵对象的类和它的绘制器以及行为都是解耦的。
实际上是一个策略模式,因为精灵和绘制器是解耦的,

策略模式

实现一个功能有多种方案可以选择。 这些算法灵活多样可以随意互相替换。
定义: 定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换!
使用策略模式就是 把算法的使用和 算法的实现 分离!
也就是说我这里的paint方法可以选择多种绘制算法,这些算法是可以互相替代的,我们
调用painter的paint方法即可。

绘制器与策略模式:

精灵对象不需要自己完成绘制,相反,它会将绘制操作委托给另一个对象完成。 Painter对象就是一些可以互相替换着使用的绘制算法。
例如

let ballSprite=new Sprite('ball',ballPainter,[Behavior.moveGravity,Behavior.fallOnLedge]);

我定义了一个ballSprite ,传入的ballPainter就是它的绘制器,
然后在游戏引擎中

    //Paint all visible sprites
    paintSprites:function(time){
        for(let i=0;i<this.sprites.length;++i){
            let sprite=this.sprites[i];
            if(sprite.visible){
                sprite.paint(this.context);
            }
        }
    },

我们会在一个动画循环中不断调用这个paintSprite方法绘制所有的设置为可见的精灵~

精灵对象的行为

只要实现了execute方法的对象,都可以叫做行为
该方法一般会以某种方式来修改精灵的属性,比如移动其位置,或者是修改其外观。
精灵含有一个行为对象数组,它的update()方法会遍历该数组,使每个行为对象都得以执行一次
我们可以把行为封装为对象,在程序运行的时候将它添加到多个精灵之中!

精灵的行为对象运用了命令模式:

行为对象能够将某种命令封装起来,它是命令模式的一种实例。 行为对象可以被执行也可以被放在某个队列中。 

应用程序在创建精灵对象时,把这一个对象的行为数组传递给精灵的构造器。
像这样创建一个精灵:

sprite=new Sprite('runner',new SpriteSheetPainter(runningCells),[runInPlace,moveLeftToRight])

将多个行为组合起来
精灵有一个行为对象数组,可以根据需要向任何精灵对象中添加惹你数量的行为对象。
精灵的update()方法会从数组的第一个行为对象开始,一直遍历到最后一个对象依次调用execute方法。

  //Update all sprites . The sprite update() method invokes all of a
    //sprite 's behaviors  ,命令模式: update && paint
    updateSprites:function(time){
        for(let i=0;i<this.sprites.length;++i){
            let sprite=this.sprites[i];
            sprite.update(this.context,time); //(context,time)
        };
    },

简单介绍一下目前引擎支持的行为对象

  1. 将精灵从左往右移动,水平平移
  2. 普通依据重力的下落行为
  3. 上抛行为
  4. 下降到Ledge的行为

举例
看下面这个从右移动精灵到左边的行为
moveRightToLeft

moveRightToLeft={
    lastMove:0,
    reset:function(){
        this.lastMove=0;
    },
    execute:function(sprite,context,time){
        //
        let elapsed=animationTimer.getElapsedTime(),
            advanceElapsed=elapsed-this.lastMove;
        if(this.lastMove===0){//skip first time
            this.lastMove=elapsed;
        }else{
            sprite.left-=(advanceElapsed/1000)*sprite.velocityX;
            this.lastMove=elapsed;
        }
    }

},

它有一个execute方法,这是一个命令模式,
精灵的update()方法会从数组的第一个行为对象开始,一直遍历到最后一个对象依次调用execute方法。

命令模式

将“请求”封装成对象,以便使用不同的请求、队列或日志来参数化其他对象。 命令模式支持可撤销的操作。
一个命令对象通过在特定接收者上绑定一组动作来封装一个请求。 命令对象将动作和接收者包含进对象中。 这个对象只暴露出一个execute方法,当此方法被调用时,接收者会进行这些动作

//sprite对象中的update方法,会对每一个行为对象执行execute方法
//命令对象就是执行update方法的对象(即sprite精灵对象),而behavior对象就是这组动作。 
 update:function(context,time){
        for(var i=0;i<this.behaviors.length;++i){
            this.behaviors[i].execute(this,context,time);
        }
    }

好处 : 不需要知道请求的接收者是谁,也不知道被请求的操作是什么。松耦合的方式设计软件。

但是对于JS
命令模式的由来,其实是回调callback的一个面向对象的替代品。
解释
JS作为将函数作为一等公民的语言,跟策略模式一样,命令模式也是早就融入语言中。 运算块不一定要封装在command.execute方法中,也可以封装在普通函数中。 函数本身就可以被四处传递!!!!!!!

第一篇先讲到这里

猜你喜欢

转载自blog.csdn.net/jlin991/article/details/61970971
今日推荐