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

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

暂停处理

游戏业务逻辑是与React组件联系比较紧的。
暂停处理的React组件如下所示:
这里写图片描述

import React from 'react';
import './Pause.scss';
import * as MiniGame from '../../miniGame';
//only div
const s={
    toast:'pauseToast',
    toastOrigin:'toast',
    pause:'pause',
    pauseButton:'pauseButton'
}
let stylePause={
    borderLeft:'10px solid white',
    borderRight:'10px solid white',
    display:'inline',
    position:'absolute',
    left:'6%',
    top:'12%',
    width:'auto',
    height:'auto',
    color:'transparent',
    cursor:'pointer',
    zIndex:'1000'
}

class Pause extends React.Component{
    constructor(props){
        super(props);
        this.state={
            value:'',
            display:'none'
        };
        this.handleClick=this.handleClick.bind(this);

    }

    componentWillMount(){

    }

    componentDidMount(){
        //window.onblur=MiniGame.windowOnBlur(this);
        //window.onfocus=MiniGame.windowOnFocus(this);
        console.log('work');
        //document.addEventListener('blur', ()=>{MiniGame.windowOnBlur(this)} );
        window.onblur=()=>{
            //console.log(this);  if you don't use arrow function ,then this -> window
            //now this -> Pause object -- a React Component
            MiniGame.windowOnBlur(this);
        }
        window.onfocus=()=>{
            MiniGame.windowOnFocus(this);
        }
    }

    handleClick(){
        MiniGame.pauseToastClickHandler(this);
    }



    render(){
        const display=this.state.display;
        return(
            <div>
                <div className={s.toast+' '+s.toastOrigin} 
                        onClick={this.handleClick}
                        style={{display:display}}>
                    <p className={s.pause}>{this.props.pause.info}</p>
                    <p>{this.props.pause.start}</p>
                </div>
                <span style={stylePause} 
                            onClick={this.handleClick}>|</span>
            </div>
        );
    }
}

export default Pause;

可以看到主要是使用内联样式来显示这个组件,在componentDidMount即组件渲染完成后绑定了
window.onblur 和window.onfocus两个事件,
事件处理函数windowOnBlur和windowOnFocus在游戏逻辑中定义,所以其实我们尽量让View的逻辑轻。
同时也有一个暂停按钮用于恢复或暂停游戏。

处理函数:

    pauseToastClickHandler=function(that){
        togglePaused(that);
    }
// 离开浏览器 失去焦点时触发
function windowOnBlur(that){
   // console.log(loading,gameOver,game.paused);
    if(!loading && !gameOver&& !game.paused){
        togglePaused(that);
        let displayx=game.paused?'inline':'none';
        that.setState({  
            display:displayx
        });
    }
    //console.log('sss');
}

function windowOnFocus(that){
    if(game.paused&&!gameOver){
        togglePaused(that);
        let displayx=game.paused?'inline':'none';
        that.setState({
            display:displayx
        });
    }
    //console.log('eee');
}

//Pause and Auto-pause Event handler...................................
    togglePaused=function(that){ //that object in jsx
        game.togglePaused();
        //pausedToast.style.display = game.paused ? 'inline' : 'none';
        let displayx=game.paused?'inline':'none';
    //Checking if game is paused and trigger paused
        if(game.paused){
            Event.trigger('GamePause');
        }else{
            Event.trigger('GameRecover');
        }
        that.setState({
            display:displayx
        });

    },

屏幕失去焦点或者获得焦点时,更新组件的state,即display属性。
togglePaused处理函数负责处理暂停恢复toggle逻辑,更新了state的同时,
触发GamePause 和 GameRecover事件。

在minGame主逻辑中看到:
利用我们定义好的发布订阅者Event类,来监听这两个自定义事件

Event.listen('GamePause',()=>{Behavior.PauseHandler(ballSprite)});
Event.listen('GameRecover',Behavior.RecoverHandler);

后面我会再介绍这两个事件,主要是处理暂停和恢复的精灵的状态。

游戏结束

这里写图片描述

class GameOver extends React.Component{
    constructor(props){
        super(props);

        //this.handleOver=this.handleOver.bind(this);
        this.handleClick=this.handleClick.bind(this);
    }

    // handleOver(){
    //     MiniGame.over(this)
    // }

    handleClick(){
       // this.props.newGameClickHandler(that);
       this.props.onClick();
    }

    render(){
        const display=this.props.overDisplay;
        return(
            <div className={s.Overtoast+' '+s.toast} style={{display:display}} >
                <p className={s.title}>{this.props.over.title}</p>
                <p>
                    <input type='checkbox' />
                    {this.props.over.clear}
                </p>
                <input type='button' value={this.props.over.button} 
                                            autoFocus='true' onClick={this.handleClick}/>
            </div>
        );
    }
}

UI的逻辑主要就是现实和隐藏这个over div, 然后就是有个重启游戏的按钮事件处理。

newGameClickHandler(){
        let overDisplay= MiniGame.newGameClick();
        console.log(overDisplay);
        this.setState({
            overDisplay:overDisplay
        });
    }

处理函数主要逻辑仍然是在我们的游戏主逻辑模块MiniGame中实现的。
同时它也会更新这个组件的state状态。

//New Game..................................
//Actually this belongs to the over Compoent
function newGameClick(){
    let str='none';
    //因为setState是异步的,所以其实这里应该promise,等到
    //状态resolved才执行startNewGame()
    setTimeout(()=>{
        startNewGame();
         setTimeout(()=>{
            //这里异步执行
            resetSprite();      
          },500);
    },100);
    return str;
};

function startNewGame(){
    //highScoreParagraph.style.display = 'none';
    ////让sprite.color最快重置为[] ? 在endGame那里重置才对
    emptyArr();
    gameOver=false;
    livesLeft=1;
    score=0;
    deepCopyColor();  //copy from FILL_STYLES to leftColor
    initalColor();//initialize color
    createStages(); //create totalColor
    //更新分数牌
    Event.trigger('LoadScore');
    //重置sprite应该在ledgeColor更新之后!

}

重启游戏主要是复位精灵,重置情况之前使用的颜色数组,要重新去生成一个颜色序列数组,并更新游戏的一些状态参数,比如 liveLeft,score等。
重启这个游戏就会把over的div隐藏起来了。 同样也是通过setState实现,因为React改变一个组件的状态state只能通过setState方法实现!

其他组件CSS

其他组件是进度组件和 Score分数组件
我们后面专门讲React在本项目的应用的时候专门再讲。
主要也是div的显示和隐藏, 其实组件中定义了多少state,就应该有相应的处理函数来处理。
这里主要讲一下绘制一个Loading按钮和 绘制气球的CSS
我们使用气球来作为得到的分数。

绘制三角形箭头

这里写图片描述

$CANVAS_WIDTH:600px;

.Progress-root {
   position:relative;
   width:$CANVAS_WIDTH;

}

.Progress-title {
   font: 16px Arial;
}

.Progress-root p {
   margin-left: 10px;
   margin-right: 10px;
   color: black;
}

.Progress-button {
   padding:{
      top: 50%;
   }
   margin:{
      top:50%;
   } 
   padding: 20px;
   position: absolute;
   left:40%;
   top: $CANVAS_WIDTH/2;
   width: 100px;
   height: 80px;
   display: block;
   background:{
      color:white;
   }
   margin:0 auto;
   border-radius:20px;
   box-shadow:1px 1px 5px #999;

}
.Progress-input{
   position:absolute;
   width:0;
   height:0;
   //20% depends on the parent node 's width
   margin:{
      left:20%;
      top:20%;
   }
   border:25px solid transparent;
   border-left:50px solid #00CC00;
   background-color:transparent;
   content:'  ';
}

绘制三角形就是Progress-input绘制,利用border,但是把width和height设置为0
想象就能知道现在就是4个三角形构成这个border了,我们只需要定义一个border-left即可
content:’ ’ 是为了兼容性考虑。

绘制气球

这里写图片描述

let styleObj={
    width:'30px',
    height:'30px',
    padding:'15px',
    borderRadius:'120px',
    borderLeft:'1px solid black',
    background:'',
    position:'absolute',
    left:'',
    top:'100px'
},
hrObj={
   width:'1px',
   height:'100px',
   position:'absolute',
   top:0,
   left:''
}

利用hr标签作为气球的竖线。
设置它的宽度为1,然后高度足够高,绝对定位。 left会在后面随着加入的气球越来越多而动态修改。
气球简单的使用borderRadius来设置即可。 同样它的left坐标后面也是要动态修改,还有它的backgroundColor也是会根据精灵得到的颜色来设置。

React绑定气球改变事件

    componentDidMount(){
        //suitable to set listen
        Event.listen('updateLineBallObj',(realMyObj)=>{
            this.setState({
                arrObj:realMyObj
            });
            //console.log(realMyObj);
        });
    }

组件生命周期中的渲染完成后,监听这个updateLineBallObj事件,一旦这个事件发生了
就说明组件state改变。

function updateMyRealColor(){ //countRect realMyColor myColor
    let leftOffset=calculLeftOffset();
    if(countRect%10===0&&countRect>=10){
        let color=myColor.shift();
        if(color){ //if color==undefined, then escape it 
            let tmpObj={color:color,left:leftOffset};
            realMyObj.push(tmpObj);
            //console.log(ballSprite.color);
            ballSprite.color.push(color);
            //触发更新分数,事件在ScoreToast中监听
            Event.trigger('LoadScore');
        }
        //realMyColor.push(color);//adding new color 
    }
    if(realMyObj.length<11){//if length >11
        Event.trigger('updateLineBallObj',realMyObj);
    }
}

这个处理函数主要就是增加气球,每隔10个矩形增加一个气球,气球即tmpObj对象来存储它的两个属性
left它的偏移位置以及color它的颜色。 颜色是在myColor中获取。同时精灵ballSprite.color也压入这个颜色,表示当前精灵拥有的颜色, 而得到的分数即气球数量。

计算气球偏移

function calculLeftOffset(){
    //len is changing all the time. leftOffset is a local variable
    let len=realMyObj.length,leftOffset;
    if(len===1){
        leftOffset=250;
        return leftOffset;
    }
    if(len<7){ //already draw 1?
        leftOffset=250-(len-1)*45;
    }else {
        leftOffset=250+(len-6)*45;
    }
    return leftOffset;
}

根据canvas的宽度,均匀分布这些气球。 设置一个初始偏移,然后后面的气球按照确定的间隔
跟随在后面。 我的算法是:先将前7个气球绘制在第一个的左边,后面的绘制在右边。
间隔都是一样的。

Behavior行为模块设计

精灵(小矩形)和大矩形的碰撞检测

逻辑框图
这里写图片描述

  1. 检测碰撞的同时需要确定精灵是否在ledge大矩形上面
  2. 对于行为的每次在动画循环中执行,每次都先判断小矩形(球)是否在下降
    如果是在下降,那就判断它降落在哪个ledge,也就是碰撞检测,检测在哪个大矩形上面。

因为我判断碰撞时,应该是有三种情况 (使用identity函数判断)

  • 小矩形刚好落在矩形内,没有超出它的边缘,
  • 小矩形在左侧,压在左边缘
  • 小矩形在右边缘,压在右边缘。

判断规则
所以我们需要判断精灵(小矩形)到底在哪个矩形上占据的位置更多,长度更多。

function identify(sprite,ledge){
    let ledgeRight=ledge.left+ledge.width,
        spriteRight=sprite.left+sprite.width;
    if(ledge.left<=sprite.left&&ledgeRight>=spriteRight){
        //completely inside the ledge!
        return 0;
    }else if(ledgeRight>spriteRight&&ledge.left>sprite.left){
        //over the range of left side ,超出左边边缘了。但仍然算碰撞到
        return -1;
    }else{
        //超出右边边缘了,但仍然是属于碰撞
        return 1;
    }
}

本质就是判断精灵的左边坐标,右边坐标和ledge的左边坐标,右边坐标相对位置。

然后detectRange就是根据这个identity返回的flag,判断这三种情况。

function detectRange(sprite,ledge,ledgeArr){
    //ledge: current ledge, ledgeArr contains all the ledges 
    //we need to compare current ledge and next ledge
    //We don't know the hitLedge is on the left side of the sprite or the right side. so we need to identify. 
    let index=ledgeArr.indexOf(ledge),
        ledgeRight=ledge.left+ledge.width,
        spriteRight=sprite.left+sprite.width,
        leftWidth,
        rightWidth;
    let flag=identify(sprite,ledge);
    index=1;
    //supposed index===1
    if(flag===0){
        return index;
    }
    //flag===1 means on the right side , -1 means on the left side 
    leftWidth=(flag===1)?(ledge.width-(sprite.left-ledge.left)):
                                    (ledge.left-sprite.left);
    rightWidth=(flag===1)?(spriteRight-ledgeRight):
                                (ledge.width-(ledgeRight-spriteRight));

    if(flag===1){
        //Math.max(leftWidth,rightWidth);//return the larger number
        return (leftWidth>rightWidth)?(index-1):(index);
    }else{
        return (leftWidth>rightWidth)?(index):(index+1);
    }
}

如果是情况A,就返回当前下标
如果是flag===1 表示小球在矩形右侧,那就判断一下,超出了多少,
如果占据当前矩形的范围更大就返回index,否则返回index+1。 (实际就是求左边占据的宽度leftWidth 和 右边占据的宽度 rightWidth进行比较~)
如果 flag===-1 表示在矩形ledge左侧,判断方式同上!

精灵重力和上抛行为

精灵行为的逻辑框图
这里写图片描述

此处检测精灵的位置是个难点,外接矩形 碰撞检测,检测它到底属于哪个矩形上面,然后才是比较颜色。

(这种预测的检测方式其实不够准确。 )

暂停与恢复是一个难点:
使用了一个栈来保存暂停之前的精灵的状态,一个暂停的处理函数,每次暂停时判断一下当前的精灵是否处于
矩形的上方,然后保存这个精灵的状态,直接整个精灵push到stack里面。 每次暂停恢复就从栈中弹出精灵
此处弹出的是最后一个精灵状态,不管暂停push压入了多少个sprite状态,只取最后一个精灵状态,包括top
velocityY等。然后就启动下降的动画计时器。

定义精灵的物理效果:

上抛运动

function tapSpeedingFalling(sprite,fps){
    sprite.top+=sprite.velocityY/fps;///this.fps;
    //falling equation
    //console.log(sprite.velocityY);
    sprite.velocityY=(GRAVITY_FORCE)*
        (fallingAnimationTimer.getElapsedTime()/1000) + TAP_VELOCITY;
    // console.log(sprite.velocityY);
    if(sprite.top>canvas.height){
        stopFalling(sprite);
    }      
}

普通下落

function normalFalling(sprite,fps){
    sprite.top+=sprite.velocityY/fps;
    sprite.velocityY=(GRAVITY_FORCE)*
                    (fallingAnimationTimer.getElapsedTime()/1000);
    if(sprite.top>canvas.height){//直到大于canvas.height
        stopFalling(sprite);
    }
}

游戏演示地址

www.linzhida.cc

项目github地址
https://github.com/spade69/lonelyRoutine

猜你喜欢

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