暂停处理
游戏业务逻辑是与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);
}
}
游戏演示地址
项目github地址
https://github.com/spade69/lonelyRoutine