Какое-то время назад решил написать небольшое приложение, чтобы потренироваться работе с вебсокетами. Из питоновских фреймворков мне показалось удобней изкоробочная поддержка их в tornado. Поскольку игрушка предельно простая, может кому-то показаться полезной как пример. Это многопользовательская «змейка».
Весь «фронт» умещается в один html файл. Вот скрипт из него
Мы получаем координаты всех змеек и яблок из вебсокета и рисуем на canvas.
При нажатии стрелок мы отправляем новое направление движения на сервер(если оно изменилось).
Теперь, серверная часть. Она тоже небольшая.
Всех игроков мы храним в поле класса GameSocketHandler, а я его экземляры хранят о них данные и связаны с вебсокетом. Каждые 100 милисекунд вызывается функция game_tick, которая двигает змеек и обнаруживает коллизии.
Целиком исходник можно забрать тут
Весь «фронт» умещается в один 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>';
}
}
}
Мы получаем координаты всех змеек и яблок из вебсокета и рисуем на canvas.
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, а я его экземляры хранят о них данные и связаны с вебсокетом. Каждые 100 милисекунд вызывается функция game_tick, которая двигает змеек и обнаруживает коллизии.
Целиком исходник можно забрать тут
Комментарии (3)
VioletGiraffe
16.01.2018 17:19А ведь можно было бы добавить объяснений, и получилась бы отличная статья/введение/туториал для начинающих, а также для ничего не смыслящих в Питоне/вебе/вебе на Питоне :)
gnunixon
Код читается легко, но лучше бы комментариев в него добавить и демо выставить куда-нибудь. А так — молодец :-)