基於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行為模組設計
精靈(小矩形)和大矩形的碰撞檢測
邏輯框圖:
- 檢測碰撞的同時需要確定精靈是否在ledge大矩形上面
- 對於行為的每次在動畫迴圈中執行,每次都先判斷小矩形(球)是否在下降
如果是在下降,那就判斷它降落在哪個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);
}
}