1. 程式人生 > >基於Canvas和React極簡遊戲(二)

基於Canvas和React極簡遊戲(二)

暫停處理

遊戲業務邏輯是與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);
    }
}

遊戲演示地址