基于Canvas和React极简游戏(一)

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

游戏设计思路

首先游戏骨架是游戏引擎

游戏引擎包括这几个部分:

  • 游戏循环
  • 绘制精灵
  • 碰撞检测
  • 帧速率更新
  • 暂停游戏
  • 事件处理
  • 图片加载
    这个极简游戏是基于这个骨架搭建起来的,考虑到游戏引擎实际上是类似库一样的提供一些功能API让你使用,所以游戏逻辑与游戏引擎其实是可以完全解耦的。

游戏的业务逻辑主要是

  • 背景滚动的实现
  • 游戏暂停恢复的处理
  • 启动游戏(游戏加载)和重置游戏的处理
  • 分数的记录和显示,分数更新,Score模块
  • 生成符合游戏规则的颜色数组,矩形颜色显示模块
  • Canvas鼠标事件处理
  • 观察者模式事件处理模块
  • 精灵的上抛,下落行为的逻辑模块

使用React编写UI组件包括
- 游戏结束UI模块
- 暂停UI模块
- 进度条模块
- 气球以及分数显示模块
- 分数排名显示模块(暂未通过调试)

React主要作用
React主要是绘制了这几个UI组件,并且负责UI相关的逻辑处理。
包括显示和隐藏这些模块,鼠标事件响应处理,更新分数,更新气球等。

我把这个 miniGame的实现看作是业务逻辑的实现。
因为我希望我的react组件可以复用,而不是嵌入太多业务逻辑的代码。
所以import 这个miniGame中的某几个函数给我用就行了。

游戏规则

小矩形从左往右运动,(实际是背景滚动造成的视差)
1. 小矩形自己有一个颜色list,表示自己所得到的分数,经过10个矩形就会增加一种颜色到myColor
2. 小矩形颜色与底部矩形颜色一样的话就继续保持,不一样就会掉落
3. 矩形的颜色是(随机的颜色数组ranColor+myColor) , 每隔2个矩形就添加一个随机颜色中的一个 2:1的比例吧。
4. 走完所有的矩形得到所有颜色就算赢了。一共设置了150个矩形,11种颜色。

回顾精灵

制作动画,要把这些基本功能封装为JS对象。 精灵:就是集成到动画之中的图形对象。
不影响动画背景的情况下移动精灵,并赋予他们各种行为! 不同的精灵对象有不同的行为!!!!!!!

精灵与设计模式

策略模式Strategy + 命令模式 command
策略模式用于将精灵与绘制器解耦decouple
命令模式用于实现精灵的动作

精灵绘制器
Sprite对象与绘制其内容的绘制器对象之间是解耦的 decoupled
程序可以在运行时为精灵对象动态设定 绘制器,极大提高了程序的灵活度
例如: 实现一个精灵动画制作器,每隔一段时间就将精灵的绘制器交换一次。
Painter对象只需要实现: void paint(sprite,context) 方法

所有Painter对象归纳为以下三类:
描边与填充绘制器 : canvas API绘制精灵
图像绘制器 : 绘制图像image ,例如drawImage
精灵表绘制器 : 绘制精灵表中的单个精灵

游戏中的精灵绘制函数

    //Painter
    ballPainter={
        paint:function(sprite,context){
            let x=sprite.left+sprite.width,
                y=sprite.top+sprite.height,
                width=sprite.width,
                height=sprite.height;

            context.save();
            Arc.drawRoundedRect(BALL_STROKE_STYLE,BALL_FILL_STYLES,
                                            x,y,width,height,CORNER_RADIUS)
            context.restore();
        }
    },

精灵行为与游戏循环

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

精灵行为 重力gravity行为, 二次弹跳,
上抛的行为。

底部矩形绘制

底部矩形:矩形的主颜色作为 精灵的属性,这个属性是一个数组,当精灵与这个数组包含的颜色以外的颜色碰撞时就会掉落(游戏规则之一)~
底部矩形绘制
画固定数量个,画8个,初始有4个在屏幕外,另外4个在屏幕内。但是颜色在不断随机更改

//画屏幕内
paintBottomRect=function(context,offset){
        let y=context.canvas.height-RECT_HEIGHT;//x
        //change too fast , 
        //let ranColor=generateRandom();
        //console.log('paintRect'); hundreds times~!
        for( let i=0;i<RECT_NUM;i++){//RECT_NUM*2
            let x=offset+RECT_WIDTH*i;
            Arc.drawRoundedRect(RECT_STROKE_STYLE,ranColor[i],
                                            x,y,RECT_WIDTH,
                                            RECT_HEIGHT,CORNER_RADIUS);
        }

    },    
    //画屏幕外
    paintBottomRectOff=function(context,offset){
        let y=context.canvas.height-RECT_HEIGHT;//x
        //change too fast , 
        //let ranColor=generateRandom();
        //console.log('paintRect'); hundreds times~!
        // let tmpRect=Behavior.fallOnLedge.ledgeRect;
        for( let i=0;i<RECT_NUM;i++){//RECT_NUM*2
            let x=offset+RECT_WIDTH*i;
            Arc.drawRoundedRect(RECT_STROKE_STYLE,backColor[i],
                                            x,y,RECT_WIDTH,
                                            RECT_HEIGHT,CORNER_RADIUS);
        }

    },

两个函数中都进行了对ledgeRect的color的更新!
相当于当你在画哪个数组的color的矩形的时候就对哪个进行了更新啊。
所以碰撞的时候也一定是对应的那个颜色!
我们检测不同颜色: 先检测是否在一个ledge内,然后检测是否是不同颜色,颜色不一致就会掉落,颜色符合就不会!~(记住要比较特殊情况, 颜色为undefined的时候不能比较!)
原因:动画循环是不断进行的,而生成颜色是一开始,所以要多加判断条件限制。

游戏中的碰撞检测

碰撞ledge检测,定义一个ledge对象,为为每个底部矩形都定义,然后把color也定义为它的属性,小球就会和他比较。这些ledge值都是固定的,因为几个矩形都是相对固定的,只有小球是在变化的。所以碰撞检测是每个底部矩形都需要的。
碰撞检测还需要判断对应的颜色!把这个flag修改。然后就触发了leftlive –
在生命周期中,检测到这个state为0,则游戏结束!触发高分榜的显示。

鼠标事件

然后鼠标单击事件,触发反向的加速度,不能去掉重力,这样就不符合。就是给一个反向的加速度,然后使得合力向上,然后持续一段时间,接着小球就下来了。

canvas鼠标事件:

//handle Game event
function handleGameClick(){
    //限制点击的次数,只能两次
    if(ballSprite.tapTimes<2&&!game.Paused){
        ballSprite.freeze=false;
        Event.trigger('OneTap');
        ballSprite.tapTimes++;
    }
}

触发这个精灵的上抛Tap行为~

function TapHandler(){
    let now=+new Date();
    Behavior.startFalling(ballSprite);
    Behavior.moveGravity.lastFrameTime=now;
}

开始falling动画计时。 对moveGravity行为初始化时间。
一旦发生了startFalling,也就是动画计时开始,精灵处于运动状态了,那么就会进入行为的处理了。
动画循环会不断执行moveGravity行为,上抛时就是执行tapSpeedingFalling,就是velocityY,Y轴速度
遵循V=gt+V0公式更新,然后精灵的位置由top控制,top+=V/fps , 位置以速度除以帧速率的步进更新
V0设置成一个较大的负值,表示相反的方向。(下落是正的方向) 。
实际上我们做了一个映射:
每秒移动的米数–》每秒移动的像素数
模拟重力
物体的下落, 难点在于 重力加速度是按照米或者英尺来度量的,而不是像素为单位,我们要将米转换为像素。
原理:
程序将平台到canvas底部的距离设定为10米,然后计算出这段距离所占据的像素数,根据这两个数字得出每米对应的屏幕像素个数。 然后用这个值将小球速度从每秒移动的米数 转换 为 每秒移动的像素数!

矩形颜色生成算法

游戏中颜色只有11个,每跳过15个台阶就会新出现一种颜色,跳过10*15个台阶游戏就结束了,精灵可以二次跳跃,但最多可以跳过2个台阶,也就是说在随机台阶属性的时候不能让3个连续的台阶都是不可踩的!

生成150个元素的颜色序列,矩阵按照这个序列绘制

function createStages(){
    let i=0,initial;
    initial=ranColor[0];
    myColor.push(initial);
    //Rule of color !! 
    while(i<TOTAL_RECT){ //totalColor 150
        let len=leftColor.length;
        //len>1 not len>0
        if(i%10===0&&i>10&&len>1){
            let tmpColor=generateRandom(leftColor);
            let index=leftColor.indexOf(tmpColor);
            //delete this color
            leftColor.splice(index,1);
            myColor.push(tmpColor); //adding new color to myColor
            //ballSprite.color.push(tmpColor);
        }
        //let newArr=randomColorMerge(myColor,leftColor);
        let color;
        if(i%3==0&&len>0) //每隔2个就添加一个新颜色中的一个
             color=generateRandom(leftColor) ;
        else {
             color=generateRandom(myColor)
        }
        stageColor.push(color);
        i++;
    }
}

逻辑:
stageColor数组存放最终生成的颜色序列,leftColor数组存放新颜色数组,myColor数组存放当前已经拥有的颜色。
循环直到生成150个, 每生成10个,就将新的颜色从leftColor中摘除并加入到myColor数组中。
然后每隔2个,就从新颜色中随机选择一个颜色,一般是从myColor中随机生成一个颜色加入到stageColor中。

  1. 一开始我让它隔5s增加一个新颜色到ranColor,然后在这个数组中shift一个前面的颜色,push一个新的颜色。后来发现这是不可行的。 有断续的颜色,而且也没法存全部的颜色。
  2. 我发现动画每秒画了很多帧,所以每次增加颜色很容易造成视觉跟不上,要在适当的时候增加颜色, 而且不能造成视觉差异,所以应该:
 paintBottomRect(context,0);
        //It must be context.canvas.width !if context.canvas.width-50
        //then it will be 50 gap! 
        paintBottomRect(context,context.canvas.width);

我们应该画8个,显示的时候只有4个是显示的,
每次都是画这个数组里面的举行色。

背景滚动显示颜色

重大问题就是怎么 滚动显示又不跳,不闪烁,不重复,需要判断是到了 CANVAS_WIDTH,才changeColor以及调整offset!

我们的ranColor是每经过一个CANVAS_WIDTH就移动一次。
CANVAS_WIDTH=600
delta=3 , 现在可以除尽,之前除不尽!

解决方法
A) 正确做法是 ranColor 和 backColor两个数组。
当屏幕滚动offset到CANVAS_WIDTH的时候,判断offset,让它移动回到0 offset处。
此时就是将当前backColor拷贝到ranColor,因为此时显示的是backColor的颜色,然后拷贝给ranColor让ranColor来显示backColor,而bakcColor就push stageColor的值,它保存更新的新的颜色值。 然后就可以没有跳动的滚动显示这两个数组。

B) 以前滚动背景的做法是两张图片或者 drawImage画两次来让显示连续,其实也是滚动offset,当完全显示第二张的时候,把offset滚动到第一张。

let scrollBackground=function(){
    //translate the canvas;
        //console.log('run background');
        context.save();
        //%game.context.canvas.witdh;
        translateOffset=translateOffset<context.canvas.width?
                            (translateOffset+translateDelta):0;
       // console.log(translateOffset);

        context.translate(-translateOffset,0);

// Two image actually ! Use one to cover the gap of another 
// 两张图片(两个图形),一个去弥补另一个
        paintNearCloud(context,120,20);
        paintNearCloud(context,context.canvas.width+120,20);
        //console.log(currentLedgeColor);
        //这两个判断需要gameStart为true的时候才对!
        if(translateOffset===CANVAS_WIDTH&&gameStart){
            changeColor();
        }
         //update the current Color array which relects the ledgeColor
        updateCurrentColor(translateOffset);
        //Count 
        if(translateOffset%RECT_WIDTH===0&&translateOffset>=RECT_WIDTH&&gameStart){
            countRect++;   
            //console.log(countRect);
            updateMyRealColor();
            //every RECT_WIDTH we update the rect 's left
            resetLeft();
        }

         //Paint Bottom 
        paintBottomRect(context,0);

        //It must be context.canvas.width !if context.canvas.width-50
        //then it will be 50 gap! 
        paintBottomRectOff(context,context.canvas.width); //paintBottomRect
        if(gameStart){
            updateLedgeLeft(translateDelta);
        }
        //此处更新ledgeColor
        updateLedgeColor();
        context.restore();
    },

背景滚动实现原理:
利用移动canvas绘图环境对象的原点坐标来实现背景滚动效果:
context.translate(-translateOffset,0);
绘制动画每一frame时,,把图像画在相同的坐标点上,然而由于程序平移了绘图环境对象的原点坐标~ 绘制cloud或者绘制矩形只需要按照原来的位置绘制即可,因为平移的是整个绘图环境对象的原点。
我们先在平移坐标系之前将绘图环境对象的状态保存起来,绘制后再恢复。 不影响其他部分。
随着绘图环境对象原点的移动,位于屏幕外的4个矩形渐渐滚动到屏幕内,屏幕内的4个矩形渐渐移出视野。
为什么不影响其他?
坐标的原点只在绘制背景才会移动,这是因为程序在平移坐标系之前先将绘图环境对象的状态保存了起来,在绘制又将其恢复,所以不会影响到其余部分。
程序把底部的矩形画了两遍,(0,0)画了一次 (CANVAS_WIDTH,0)又画了一次。 一开始的时候(0,0) ranColor对应的那些矩形全部可见, (CANVAS_WIDTH,0)的时候不可见。

 //Paint Bottom 
        paintBottomRect(context,0);

        //It must be context.canvas.width !if context.canvas.width-50
        //then it will be 50 gap! 
        paintBottomRectOff(context,context.canvas.width); //paintBottomRect

绘制的时候,我们会更新矩形的颜色, 每次更新是在偏移到了RECT_WIDTH的宽度才更新

if(translateOffset%RECT_WIDTH===0&&translateOffset>=RECT_WIDTH&&gameStart)

意思就是只要是RECT_WIDTH的倍数都会触发颜色的更新。

背景滚动的问题

如何消除抖动?
原理 : 程序绘制动画的每一帧的时候,都会把image画在相同的坐标上,然而由于程序平移了绘图环境对象的原点坐标,所以看起来云彩好像正在从右向左移动~

抖动解决:
动画游戏很多异步, 我们这里要用一个 setTimeout来延时这个updateBackground,50ms刚好看不出来,因为前面动画刷新是很快的,如果你马上update了,就会在前面一些帧数画了这个最新的backColor到画面中,此时draw backColor就会看到有新的颜色,然而这不是我们想要的。

function changeColor(){
    //let color=stageColor.shift();
    //let tmp=backColor.shift();
    copyToRan();
    //不要马上更新背景颜色,不然就会跟不上~
    //因为前面,动画可能还在画draw这个矩形
    setTimeout(()=>{
        updateBackColor();
    },50);
}

猜你喜欢

转载自blog.csdn.net/jlin991/article/details/63269414