3D
shooter, starting from importing cute tank models to the scene and ending with synchronizing players and bots with each other using websocket
and asyncio
and balancing them in rooms.Python3
who is interested in the topic of asynchronous applications in Python3
, WebGL
and just games, please under the cat.asyncio
, and if the first part was devoted to how best to make a modular structure like django
, on top of aiohttp( asyncio )
, then the second one already wanted to do something more creative.3D
toy, and where else if not in games, you might need asynchronous programming.memcached
.git clone
and went to an ATM. Rather, it is an attempt to make a gaming framework, a demo of asyncio
and webgl
in one bottle. There is no shop window, ratings, thoroughly tested security, etc., but, on the other hand, it seems to me that for an open sourse
project, developed occasionally, in my spare time, it turned out to be quite normal.3D
models of characters for a toy, we need models to simulate a landscape, buildings, etc.blender
, the most common are .die .obj .3ds
..3ds
, then, as a rule, the model is imported without textures, but with materials already created. In this case, we just need to load each material from the texture disk. For .obj
, as a rule, besides textures, there must be a .mtl
file, if it is present, then the probability of problems is usually less.chrome
crashes, warning that he has problems with displaying the webgl
. In this case, you should try to remove all unnecessary things in the blender, for example, if there are collisions, animations, etc.UV
sweep. To do this, you can select all objects through the outliner with cipher, they will be highlighted with a characteristic orange color and in the object
menu select the join
item.ctrl+P
to select object
in the menu. As a result, we should see in the autliner that all the objects that make up our model belong to the same parent. loader = new BABYLON.AssetsManager(scene); mesh = loader.addMeshTask('enemy1', "", "/static/game/t3/", "t.babylon");
mesh.onSuccess = function (task) { task.loadedMeshes[0].position = new BABYLON.Vector3(10, 2, 20); };
var palm = loader.addMeshTask('palm', "", "/static/game/g6/", "untitled.babylon"); palm.onSuccess = function (task) { var p = task.loadedMeshes[0]; p.position = new BABYLON.Vector3(25, -2, 25); var p1 = p.clone('p1'); p1.position = new BABYLON.Vector3(10, -2, 20); var p2 = p.clone('p2'); p2.position = new BABYLON.Vector3(15, -2, 30); };
AssetsManager
case. He draws a simple screensaver until the main part of the scene is loaded, and makes sure that everything we loader.onFinish
into loader.onFinish
. var createScene = function () { . . . } var scene = createScene(); loader.onFinish = function (tasks) { engine.runRenderLoop(function () { scene.render(); }); };
babylon.js
called heightMap . To do this, you need to apply a black-and-white picture on the soil texture depending on where the light surface is and where the dark and mountains form. hills, some of the characteristics can be specified in the parameters, the more blurred the transition between dark and white, the gentler the slope. var ground = BABYLON.Mesh.CreateGroundFromHeightMap("ground", "/static/HMap.png", 200, 200, 70, 0, 10, scene, false); var groundMaterial = new BABYLON.StandardMaterial("ground", scene); groundMaterial.diffuseTexture = new BABYLON.Texture("/static/ground.jpg", scene); ground.material = groundMaterial;
FPS
and so on.babylon.js
for the movement from the first person, there are two cameras:FreeCamera
- as a rule, is the parent of the character and the character just follows it, it is very convenient to use for the turn of technology, for people and for all flying, FreeCamera
has the ability to adjust the inertia and speed of movement, which is also very important.FollowCamera
- on the contrary, it is a camera that follows some object; it is more convenient to use it for cases when the control from the mouse and keyboard is different. That is, the review does not depend on the direction of movement. //FollowCamera var camera = new BABYLON.FollowCamera("camera1", new BABYLON.Vector3(0, 2, 0), scene); camera.target = mesh; ``````javascript //FreeCamera var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3( 0, 2, 0), scene); mesh.parent = camera;
babylon.js
has a good sound management API . The topic of sound management is quite extensive, so we’ll look at just a few small examples. var music = new BABYLON.Sound("Music", "music.wav", scene, null, { playbackRate:0.5, // . volume: 0.1, // . loop: true, // . autoplay: true // . });
var gunS = new BABYLON.Sound("gunshot", "static/gun.wav", scene); window.addEventListener("mousedown", function (e) { if (!lock && e.button === 0) gunS.play(); });
var ms = new BABYLON.Sound("mss", "static/move.mp3", scene, null, { loop: true, autoplay: false }); document.addEventListener("keydown", function(e){ switch (e.keyCode) { case 38: case 40: case 83: case 87: if (!ms.isPlaying) ms.play(); break; } }); document.addEventListener("keyup", function(e){ switch (e.keyCode) { case 38: case 40: case 83: case 87: if (ms.isPlaying) ms.pause(); break; } });
babylon.js
. Probably the easiest is to create another camera, place it on top of the map and place the view from this camera in the corner we need.freeCamera
and tell her that it should be placed on top: camera2 = new BABYLON.FreeCamera("minimap", new BABYLON.Vector3(0,170,0), scene);
Y
coordinate, the fuller the overview, but the finer the details on the map. camera2.viewport = new BABYLON.Viewport(x, y, width, height);
scene.activeCameras.push(camera); scene.activeCameras.push(camera2);
websoket
, the player performs an action, mouse rotation or keystroke, an event is hung up on this movement, in which the coordinates of the player’s location are transmitted to the server, and the server transmits them to all game participants who are in this game. the room.FreeCamera
, it is a parent object, therefore, we use its coordinates. For example:camera.cameraRotation
- contains the X
and Y
coordinates of the rotation along the axes.camera.position
- contains X
, Y
and Z
coordinates of the mesh location on the map.action
, then what action did the player perform and in accordance with this call the desired function, event handler: async def game_handler(request): . . . async for msg in ws: if msg.tp == MsgType.text: if msg.data == 'close': await ws.close() else: e = json.loads( msg.data ) action = e['e'] if action in handlers: handler = handlers[action] handler(ws, e) . . .
move
came to us, it means that the player has moved to some coordinates. In the handler function of this action, we simply assign these coordinates to the Player
class, it processes them and returns them, and we transmit them to all the other players in the room at the moment. def h_move(me, e): me.player.set_pos(e['x'], e['y'], e['z']) mess = dict(e="move", id=me.player.id, **me.player.pos_as_dict) me.player.room.send_all(mess, except_=(me.player,))
Player
class with coordinates is doing now is simply passing them to the bot so that it will be guided by them. But ideally, he should check the entire map with the coordinates, and if the player hit the obstacle, he, for example, to avoid cheating, should not let him seep through the wall, if the script is changed on the client. class Player(list): . . . def __init__(self, client, room, x=0, y=0, z=0, a=0, b=0): list.__init__(self, (x, y, z, a, b)) self._client = client self._room = room Player.last_id += 1 self._id = Player.last_id room.add_player(self) . . . def set_rot(self, a, b): self[3:5] = a, b def getX(self): return self[0] . . . def setX(self, newX): self[0] = newX . . . x = property(getX, setX) . . . @property def pos_as_dict(self): return dict(zip(('x', 'y', 'z'), self.pos))
Player
class we use property
, for more convenient work with coordinates. Who cares, Habré was the best material on this topic./pregame
. When you press a button, ajax
triggered, which, if successful, redirects the player to the desired room. if (data.result == 'ok') { window.location = '/game#'+data.room;
rooms
dictionary, which contains a list of all the rooms and players located in them, and if the number of players does not exceed the specified value, then we return the room id
client. And if the number of players is more, then we create a new room. def check_room(request): found = None for _id, room in rooms.items(): if len(room.players) < 3: found = _id break else: while not found: _id = uuid4().hex[:3] if _id not in rooms: found = _id
Room
class is responsible for working with rooms. His general scheme of work looks like this:Player
class, perhaps the whole scheme does not look quite linear, but in the end it will make it quite convenient to write such chains: # me.player.room.send_all( {"e" : "move", . . . }) # me.player.room.players # me.player.room.add_player(self) # me.player.room.remove_player( me.player )
me.player
, because for some of my colleagues this caused questions, me
is a socket that is passed as a parameter in functions that serve events: def h_new(me, e): me.player = Player(me, Room.get( room_id ), x, z)
player = Player( Room.get(room_id), x, z) player.me = me me.player = player
player
module and the .player
object of me
, both are equal and refer to the same object in memory that will exist as long as there is at least one link. >>> a = {1} >>> b = a >>> b.add(2) >>> b {1, 2} >>> a {1, 2}
b
and a
are simply references to one common value. >>> a.add(3) >>> a {1, 2, 3} >>> b {1, 2, 3}
>>> class A(object): ... pass ... >>> a = A() >>> a.player = b >>> a.player {1, 2, 3} >>> b {1, 2, 3} >>> a.__dict__ {'player': {1, 2, 3}}
__dict__
dictionary __dict__
a
, but instead we created another that belongs to the newly created object, and in fact lies in the __dict__
dictionary of this object. >>> a <__main__.A object at 0x7f3040db91d0>
mess = dict(e="move", bot=1, id=self.id, **self.pos_as_dict) self.room.send_all(mess, except_=(self,))
Room
__init__
Bot
. def __init__
Bot
asyncio.async(self.update())
, .await
, -. , class
, , . , await
, .__next__()
. — next
async
— . import asyncio async def test( name ): ctr = 0 while True: await asyncio.sleep(2) ctr += 1 print("Task {}: test({})".format( ctr, name )) asyncio.ensure_future( test("A") ) asyncio.ensure_future( test("B") ) asyncio.ensure_future( test("C") ) loop = asyncio.get_event_loop() loop.run_forever( )
asyncio.ensure_future
, , asyncio.sleep(2)
). , , , , , , , . , , .Nginx
, websocket
http
. , , : server { server_name aio.dev; location / { proxy_pass http://127.0.0.1:8080; } }
5.5.5.10
loalhost
. server { server_name aio.dev; location / { proxy_pass http://5.5.0.10:8080; } }
nginx
, proxy_pass
http://127.0.0.1:8080
Nginx
-a, , : server { server_name aio.dev; location / { proxy_pass http://127.0.0.1:8080; } location /ws { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
var uri = "ws://aio.dev:80/ws"
, Nginx
80 , listen
.Nginx
-, websoket
-, http
.memcached
async def
. - , — , , .., , . memcached svetlov , .memcached
pickle
. — aiohttp
, CIMultiDict
, , Cython
aiohttp
. dct = CIMultiDict() print( dct ) <CIMultiDict {}> dct = MultiDict({'1':['www', 333]}) print( dct ) <MultiDict {'1': ['www', 333]}> dct = MultiDict([('a', 'b'), ('a', 'c')]) print( dct ) <MultiDict {'a': 'b', 'a': 'c'}> dct = dict([('a', 'b'), ('a', 'c')]) print( dct ) {'a': 'c'}
pickle
. , , CIMultiDict
pickle
. d = MultiDict([('a', 'b'), ('a', 'c')]) prepared = [(k, v) for k, v in d.items()] saved = pickle.dumps(prepared) restored = pickle.loads(saved) refined = MultiDict( restored )
def cache(name, expire=0): def decorator(func): async def wrapper(request=None, **kwargs): args = [r for r in [request] if isinstance(r, aiohttp.web_reqrep.Request)] key = cache_key(name, kwargs) mc = request.app.mc value = await mc.get(key) if value is None: value = await func(*args, **kwargs) v_h = {} if isinstance(value, web.Response): v_h = value._headers value._headers = [(k, v) for k, v in value._headers.items()] await mc.set(key, pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL), exptime=expire) if isinstance(value, web.Response): value._headers = v_h else: value = pickle.loads(value) if isinstance(value, web.Response): value._headers = CIMultiDict(value._headers) return value return wrapper return decorator
from core.union import cache @cache('list_cached', expire=10 ) async def list_tags(request): return templ('list_tags', request, {})
WebGL
asyncio
. , . , , .Source: https://habr.com/ru/post/252575/
All Articles