竜巻でマルチプレイヤーヘビを書く

しばらく前に、Webソケットの操作を練習する小さなアプリケーションを作成することにしました。 Pythonフレームワークの中で、竜巻でサポートする方が便利だと思われました。 おもちゃは非常にシンプルなので、例として誰かに役立つように思えるかもしれません。 これはマルチプレイヤーの「ヘビ」です。







「フロント」全体が1つのhtmlファイルに収まります。 ここからスクリプトです



var canvas = document.getElementById('canvas'), c = canvas.getContext('2d'), direction='up', nick='Anonymous'; c.lineWidth = 1; var snakes=[ ]; var apples=[ [10,10], [2,2] ]; var colors = ['red', 'blue', 'green', 'black', 'purple', 'teal', 'navy', 'lime', 'olive', 'maroon', 'aqua'] function redraw(){ c.clearRect(0, 0, canvas.width, canvas.height); c.stroke(); for(var j=0; j<snakes.length; j++){ c.fillStyle=colors[j]; for(var i=0; i<snakes[j].length; i++) { c.fillRect(snakes[j][i][0]*10,snakes[j][i][1]*10, 10,10); } c.stroke(); } for(var i=0; i<apples.length; i++){ c.strokeStyle="#FF00DD"; c.beginPath(); c.arc(apples[i][0]*10+5,apples[i][1]*10+5,5,0,2*Math.PI); c.stroke(); } } var updater = { socket: null, start: function() { if(updater.socket && updater.socket.readyState !== updater.socket.CLOSED) return; var url = "ws://" + location.host + "/gamesocket"; updater.socket = new ReconnectingWebSocket(url); updater.socket.onmessage = function(event) { updater.showMessage(JSON.parse(event.data)); redraw(); } updater.socket.onopen = function(event){ updater.socket.send(JSON.stringify({'nick': nick})); } }, showMessage: function(message) { snakes = message.snakes; apples = message.apples; document.getElementById('scores').innerHTML=""; for(var i=0; i<message.scores.length; i++){ document.getElementById('scores').innerHTML+=message.scores[i][0]+': '+message.scores[i][1]+'<br>'; } } }
      
      





Webソケットからすべてのヘビとリンゴの座標を取得し、キャンバスに描画します。



  document.onkeydown = function(e){ var keys = {37:'left', 39:'right', 38:'up', 40:'down'}; var k = keys[e.keyCode]; if(k && k != direction){ if(direction == 'up' && k == 'down') return; if(direction == 'down' && k == 'up') return; if(direction == 'left' && k == 'right') return; if(direction == 'right' && k == 'left') return; direction = keys[e.keyCode]; updater.socket.send(JSON.stringify({direction:direction})); } } window.onload = function(){ nick = window.prompt("Enter your name","Anonymous"); if(!nick) nick = 'Anonymous'; updater.start(); }
      
      





矢印をクリックすると、新しい移動方向がサーバーに送信されます(変更されている場合)。



さて、サーバー側。 彼女も小さいです。



 import logging import tornado.escape import tornado.ioloop import tornado.options import tornado.web import tornado.websocket from tornado import gen import os.path import uuid import json import random from datetime import datetime from tornado.options import define, options SIZE = 100, 60 define("port", default=8000, help="run on the given port", type=int) class Application(tornado.web.Application): def __init__(self): handlers = [ (r"/", IndexHandler), (r"/gamesocket", GameSocketHandler), (r"/files/(.*)", tornado.web.StaticFileHandler, {'path': 'files'}), ] settings = dict( cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), xsrf_cookies=True, autoreload=True, debug=True, ) tornado.web.Application.__init__(self, handlers, **settings) class IndexHandler(tornado.web.RequestHandler): def get(self): self.render("index.html") class GameSocketHandler(tornado.websocket.WebSocketHandler): players = set() apples = [[random.randint(0, SIZE[0]), random.randint(0, SIZE[1])] for i in range(20) ] def get_compression_options(self): # Non-None enables compression with default options. return {} def open(self): x_real_ip = self.request.headers.get("X-Real-IP") self.ip = x_real_ip or self.request.remote_ip self.direction = 'up' self.score = 0 self.nick = 'Anonymous' self.die() GameSocketHandler.players.add(self) GameSocketHandler.send_updates() def die(self): while 1: x, y = random.randint(0, SIZE[0]), random.randint(0, SIZE[1]) if any(x == sx and y == sy for player in GameSocketHandler.players for sx, sy in player.snake): continue if any(x == sx and y == sy for sx, sy in GameSocketHandler.apples): continue break self.snake = [[x, y]] @classmethod def add_apple(cls): while 1: x, y = random.randint(0, SIZE[0]), random.randint(0, SIZE[1]) if any(x == sx and y == sy for player in GameSocketHandler.players for sx, sy in player.snake): continue if any(x == sx and y == sy for sx, sy in GameSocketHandler.apples): continue break cls.apples.append([x, y]) def on_close(self): GameSocketHandler.players.remove(self) @classmethod def send_updates(cls): #logging.info("sending message to %d waiters", len(cls.players)) data = { 'snakes': [player.snake for player in cls.players], 'apples': cls.apples, 'scores': [[player.nick, player.score] for player in cls.players] } for waiter in cls.players: try: data['head'] = waiter.snake[-1] waiter.write_message(data) except: logging.error("Error sending message", exc_info=True) def on_message(self, message): message = json.loads(message) logging.info("Got message %r" % message) if 'direction' in message: if message['direction'] not in ['up', 'left', 'right', 'down']: return self.direction = message['direction'] elif 'nick' in message: self.nick = message['nick'].replace('<', '').replace('>', '') def game_tick(): for player in GameSocketHandler.players: d = {'up': (0,-1), 'down': (0,1), 'left': (-1, 0), 'right': (1,0)} player.snake.append([player.snake[-1][0]+d[player.direction][0], player.snake[-1][1]+d[player.direction][1]]) if player.snake[-1] in GameSocketHandler.apples: GameSocketHandler.apples.remove(player.snake[-1]) GameSocketHandler.add_apple() player.score += 1 else: player.snake.pop(0) if player.snake[-1][0] < 0 or player.snake[-1][1] < 0 or player.snake[-1][0] >= SIZE[0] or player.snake[-1][1] >= SIZE[1]: player.die() for enemy in GameSocketHandler.players: if enemy != player and player.snake[-1] in enemy.snake: player.die() if player.snake[-1] in player.snake[:-1]: player.die() GameSocketHandler.send_updates() def main(): tornado.options.parse_command_line() app = Application() app.listen(options.port) tornado.ioloop.PeriodicCallback(game_tick, 100, io_loop = tornado.ioloop.IOLoop.current()).start() tornado.ioloop.IOLoop.current().start() if __name__ == "__main__": main()
      
      





すべてのプレーヤーをGameSocketHandlerクラスのフィールドに格納し、そのインスタンスはそれらに関するデータを格納し、Webソケットに関連付けられます。 100ミリ秒ごとにgame_tick関数が呼び出されます。この関数は、蛇を動かして衝突を検出します。



ここでソース全体を取得できます



All Articles