websocket实战(4) websocket版贪食蛇游戏(tomcat官方自带)
websocket实战(1) 入门
websocket实战(2) 信息处理发送、接收和编码
websocket实战(3) 错误处理及配置管理
通过前面3篇的阐述,相信可以构建一个简单的socket应用了。当然,也会遗漏了许多知识点,相信会在以后分享的实例中捎带说明下。
本文的主要是分析下tomcat官方自带的贪食蛇游戏。为什么选择分析这个项目呢。
贪食蛇游戏规则,人人明白,业务方面不需要过多解释(当然这款websocket版的游戏规则也有一定特色)。
游戏设计简单,一个对象足以完成游戏,但不涉及到一些复杂的逻辑算法。
通过游戏,有很好的代入感
1.游戏规则介绍
1.能够实现贪吃蛇自动向前移动,一旦贪食蛇选择了方向,贪食蛇就按所选方向开始运动,可以任意。移动方向为贪吃蛇当前行走方向。
2.游戏通过键盘的上下左右四个方向控制贪吃蛇当前行走方向。(没有可以吃的食物)。
3.支持对战功能,如果发生碰撞情况,后蛇会自杀,重置信息,重新来玩。
4.如果移动出画布外,从对立方向进入,移动方向不变。
界面是"群蛇乱舞”界面。
2.贪食蛇设计
贪食蛇状态快照
贪食蛇类图
贪食蛇:有几个重要属性。颜色,头(head),身体(tail),行动方向。
颜色:随机生成。
头&身体:决定蛇的长度,在画布中的位置。还有决定是否发生碰撞。有(x,y)坐标说明。
行动方向:东西南北四个方向。
重点说一下和websocket相关的信息。贪食蛇的session属性。
session主要负责贪食蛇状态信息的传播,将自己的颜色和位置信息传递到前端。
传播时机
状态变化要传播(kill,join,..)
位置变化要传播(包括方向,其实也是状态变化)
重置要传播(也是状态变化)
分析序列图得知,其实作为游戏的websocket的EndPoint,做的事情很简单。两件事
有新需求:创建贪食蛇,发送渲染命令(join)
响应客户端的命令(方向命令)
不难分析,游戏贪食蛇的移动,是应该有定时器驱动的,所有贪食蛇位置的变化,都是通过SnakeTimer驱动的。然后更新位置信息,最后调用贪食蛇,将自己信息传递到前端。所以定时器,需要维护贪食蛇的聚合信息。
1.贪食蛇聚合信息维护(CRD,没有更新,贪食蛇信息的更新不属于聚合信息范畴)
protectedstaticsynchronizedvoidaddSnake(Snakesnake){if(snakes.size()==0){startTimer();}snakes.put(Integer.valueOf(snake.getId()),snake);}protectedstaticCollection<Snake>getSnakes(){returnCollections.unmodifiableCollection(snakes.values());}protectedstaticsynchronizedvoidremoveSnake(Snakesnake){snakes.remove(Integer.valueOf(snake.getId()));if(snakes.size()==0){stopTimer();}}
2. 消息广播(将贪食蛇最新状态信息,实时广播到前端)
就是调用snake自动发送,不难猜,调用session相关的方法。
//SnakeTimer.javaprotectedstaticvoidbroadcast(Stringmessage){for(Snakesnake:SnakeTimer.getSnakes()){try{snake.sendMessage(message);}catch(IllegalStateExceptionise){//AnISEcanoccurifanattemptismadetowritetoa//WebSocketconnectionafterithasbeenclosed.The//alternativetocatchingthisexceptionistosynchronise//thewritestotheclientsalongwiththeaddSnake()and//removeSnake()methodsthatarealreadysynchronised.}}}//Snake.javaprotectedvoidsendMessage(Stringmsg){try{session.getBasicRemote().sendText(msg);}catch(IOExceptionioe){CloseReasoncr=newCloseReason(CloseCodes.CLOSED_ABNORMALLY,ioe.getMessage());try{session.close(cr);}catch(IOExceptionioe2){//Ignore}}}
实时更新位置信息
websocket.snake.SnakeTimer.tick()
protectedstaticvoidtick(){StringBuildersb=newStringBuilder();for(Iterator<Snake>iterator=SnakeTimer.getSnakes().iterator();iterator.hasNext();){Snakesnake=iterator.next();snake.update(SnakeTimer.getSnakes());sb.append(snake.getLocationsJson());if(iterator.hasNext()){sb.append(',');}}broadcast(String.format("{'type':'update','data':[%s]}",sb.toString()));}
按方向计算贪食蛇头下一个的位置
websocket.snake.Location. getAdjacentLocation(Direction direction)
没有方向,不变化位置。
publicLocationgetAdjacentLocation(Directiondirection){switch(direction){caseNORTH:returnnewLocation(x,y-SnakeAnnotation.GRID_SIZE);caseSOUTH:returnnewLocation(x,y+SnakeAnnotation.GRID_SIZE);caseEAST:returnnewLocation(x+SnakeAnnotation.GRID_SIZE,y);caseWEST:returnnewLocation(x-SnakeAnnotation.GRID_SIZE,y);caseNONE://fallthroughdefault:returnthis;}}
websocket.snake.Snake. update(Collection<Snake> snakes)
publicsynchronizedvoidupdate(Collection<Snake>snakes){LocationnextLocation=head.getAdjacentLocation(direction);if(nextLocation.x>=SnakeAnnotation.PLAYFIELD_WIDTH){nextLocation.x=0;}if(nextLocation.y>=SnakeAnnotation.PLAYFIELD_HEIGHT){nextLocation.y=0;}if(nextLocation.x<0){nextLocation.x=SnakeAnnotation.PLAYFIELD_WIDTH;}if(nextLocation.y<0){nextLocation.y=SnakeAnnotation.PLAYFIELD_HEIGHT;}if(direction!=Direction.NONE){tail.addFirst(head);if(tail.size()>length){tail.removeLast();//这一步很关键,实现动态位置变化,否则蛇就无限增长了}head=nextLocation;}//处理蛇是否发生碰撞handleCollisions(snakes);}
判断是否发生碰撞
判断和其他,是否发生重叠。是否迎头碰撞,还是头尾碰撞。
privatevoidhandleCollisions(Collection<Snake>snakes){for(Snakesnake:snakes){booleanheadCollision=id!=snake.id&&snake.getHead().equals(head);booleantailCollision=snake.getTail().contains(head);if(headCollision||tailCollision){kill();//牺牲自己,触发dead类型信息if(id!=snake.id){snake.reward();//成全别人,让别人长度增加1.触发kill类型信息}}}}
主要业务逻辑就分析完毕了。有对canvas感兴趣的,可以关注前端js.
varGame={};Game.fps=30;Game.socket=null;Game.nextFrame=null;Game.interval=null;Game.direction='none';Game.gridSize=10;functionSnake(){this.snakeBody=[];this.color=null;}Snake.prototype.draw=function(context){for(varidinthis.snakeBody){context.fillStyle=this.color;context.fillRect(this.snakeBody[id].x,this.snakeBody[id].y,Game.gridSize,Game.gridSize);}};Game.initialize=function(){this.entities=[];canvas=document.getElementById('playground');if(!canvas.getContext){Console.log('Error:2dcanvasnotsupportedbythisbrowser.');return;}this.context=canvas.getContext('2d');window.addEventListener('keydown',function(e){varcode=e.keyCode;if(code>36&&code<41){switch(code){case37:if(Game.direction!='east')Game.setDirection('west');break;case38:if(Game.direction!='south')Game.setDirection('north');break;case39:if(Game.direction!='west')Game.setDirection('east');break;case40:if(Game.direction!='north')Game.setDirection('south');break;}}},false);if(window.location.protocol=='http:'){Game.connect('ws://'+window.location.host+'/wsexample/websocket/snake');}else{Game.connect('wss://'+window.location.host+'/wsexample/websocket/snake');}};Game.setDirection=function(direction){Game.direction=direction;Game.socket.send(direction);Console.log('Sent:Direction'+direction);};Game.startGameLoop=function(){if(window.webkitRequestAnimationFrame){Game.nextFrame=function(){webkitRequestAnimationFrame(Game.run);};}elseif(window.mozRequestAnimationFrame){Game.nextFrame=function(){mozRequestAnimationFrame(Game.run);};}else{Game.interval=setInterval(Game.run,1000/Game.fps);}if(Game.nextFrame!=null){Game.nextFrame();}};Game.stopGameLoop=function(){Game.nextFrame=null;if(Game.interval!=null){clearInterval(Game.interval);}};Game.draw=function(){this.context.clearRect(0,0,640,480);for(varidinthis.entities){this.entities[id].draw(this.context);}};Game.addSnake=function(id,color){Game.entities[id]=newSnake();Game.entities[id].color=color;};Game.updateSnake=function(id,snakeBody){if(typeofGame.entities[id]!="undefined"){Game.entities[id].snakeBody=snakeBody;}};Game.removeSnake=function(id){Game.entities[id]=null;//ForceGC.deleteGame.entities[id];};Game.run=(function(){varskipTicks=1000/Game.fps,nextGameTick=(newDate).getTime();returnfunction(){while((newDate).getTime()>nextGameTick){nextGameTick+=skipTicks;}Game.draw();if(Game.nextFrame!=null){Game.nextFrame();}};})();Game.connect=(function(host){if('WebSocket'inwindow){Game.socket=newWebSocket(host);}elseif('MozWebSocket'inwindow){Game.socket=newMozWebSocket(host);}else{Console.log('Error:WebSocketisnotsupportedbythisbrowser.');return;}Game.socket.onopen=function(){//Socketopen..startthegameloop.Console.log('Info:WebSocketconnectionopened.');Console.log('Info:Pressanarrowkeytobegin.');Game.startGameLoop();setInterval(function(){//Preventserverreadtimeout.Game.socket.send('ping');},5000);};Game.socket.onclose=function(){Console.log('Info:WebSocketclosed.');Game.stopGameLoop();};Game.socket.onmessage=function(message){//_Potential_securityhole,considerusingjsonlibtoparsedatainproduction.varpacket=eval('('+message.data+')');switch(packet.type){case'update':for(vari=0;i<packet.data.length;i++){Game.updateSnake(packet.data[i].id,packet.data[i].body);}break;case'join':for(varj=0;j<packet.data.length;j++){Game.addSnake(packet.data[j].id,packet.data[j].color);}break;case'leave':Game.removeSnake(packet.id);break;case'dead':Console.log('Info:Yoursnakeisdead,badluck!');Game.direction='none';break;case'kill':Console.log('Info:Headshot!');break;}};});varConsole={};Console.log=(function(message){varconsole=document.getElementById('console');varp=document.createElement('p');p.style.wordWrap='break-word';p.innerHTML=message;console.appendChild(p);while(console.childNodes.length>25){console.removeChild(console.firstChild);}console.scrollTop=console.scrollHeight;});Game.initialize();document.addEventListener("DOMContentLoaded",function(){//Removeelementswith"noscript"class-<noscript>isnotallowedinXHTMLvarnoscripts=document.getElementsByClassName("noscript");for(vari=0;i<noscripts.length;i++){noscripts[i].parentNode.removeChild(noscripts[i]);}},false);
结论
通过阅读一些官方文档的代码,学习人家的编码风格,细节。比如线程安全方面。js的面向对象编写,很优雅。不像笔者遇到的经常看到的一个方法,一个方法式的嵌套调用,不考虑性能,就阅读起来就特别费劲。
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。