erlang и panda3d
От: neFormal Россия  
Дата: 10.07.12 20:21
Оценка: 27 (3) :)
Как можно по-быстрому, на коленке накатать простенькое приложение, совершенно не зная ни эрланга, ни панды. Скажу сразу: самое сложное — это поставить panda3d в систему. Под линухами это крайне криво выглядит.

В качестве протокола я взял msgpack. Можно было бы взять и bert, но, честно говоря, разницы между ними в использовании не особо много. Поэтому в данный момент пофиг, благо сериализация однострочная в одном единственном месте. Сам msgpack для эрланга поставляет один сорс, а для питона (ведь панда скриптуется питоном) устанавливается соответствующий пакет(для берта два пакета).

И напомню, что это proof of concept, а не рабочий код, который может попасть в production.

Сервак.


У эрланга много различных вкусных штук, но для Ъ-лентяев это значит много чтива, а это негатив. С LYSM.com, оф.доками и божьей помощью наколбасим-ка рабочего быдлокода.
Точка входа.
  Скрытый текст
-module(swsrv).
-author(neFormal).
-behaviour(application).
-export([start/2, stop/1]).

start(_, _) ->
    lobby_sup:start_link(),
    client_sup:start_link().
stop(_) ->
    ok.


С возрастом я стал сдержанней относиться к фишке с запятыми и точками. Хотя до сих пор считаю это наркотой. Но вот 4 года назад, когда впервые столкнулся с эрлангом, для меня это была проблема.
Так вот. Точка входа есть, и там встаём на прослушку сокета. Также запускаем лобби. Это такая штука, где люди могут выбрать игру, в которую играть, или создать её, или ещё что сделать. Грубо говоря, как в Quake/CS выбор сервака. Здесь эта фича номинальная и не подразумевает какой-то логики. В основном потому, что писать гуй для красивого отображения списка игр меня заломало. Поэтому везде используются грубые хоткеи типа "Создать игру 1" и "Подключиться к игре 1". Раньше я думал, что всегда делают удобный интерфейс для таких штук, но самая первая работа в геймдеве разрушила этот миф. И ведь оказалось действительно удобно забить действия на F*, даже арты это легко понимают.

Слушатель клиентов. Он же супервизор.
  Скрытый текст
-module(client_sup).
-author(neFormal).

-behaviour(supervisor).

-export([start_link/0, start_socket/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
    {ok, Port} = {ok, 4000},
    {ok, ListenSocket} = gen_tcp:listen(Port, [{active, true}, {packet, line}]),
    spawn_link(fun empty_listeners/0),
    {ok, {{simple_one_for_one, 60, 3600},
     [{socket,
      {client, start_link, [ListenSocket]}, %pass the socket
      temporary, 1000, worker, [client]}
     ]}}.

start_socket() ->
    supervisor:start_child(?MODULE, []).

%% 20 active listeners
empty_listeners() ->
    [start_socket() || _ <- lists:seq(1, 20)],
    ok.


Тут можно уже принимать 20 одновременных соединений и радоваться работе сети.

Супервизор над лобби. У него должна бы быть важная миссия — следить за возможными помирающими лобби.
  Скрытый текст
-module(lobby_sup).
-author(neFormal).
-behaviour(supervisor).

-export([start_link/0, init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
    spawn_link(fun start_lobby/0),
    {ok, {{simple_one_for_one, 60, 3600},
          [{lobby,
            {lobby, start_link, []},
            permanent,%%temporary,
            1000,
            worker,
            dynamic%%[lobby_game]
           }]
         }}.

start_lobby() ->
    supervisor:start_child(?MODULE, []).


Само лобби.
  Скрытый текст
-module(lobby).
-author(neFormal).

-behaviour(gen_server).

-export([start_link/0, list/0, create/1, join/3]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
        terminate/2, code_change/3]).

-record(player, {name, team=false, pid=false}).
-record(game, {id, players_limit, pid}).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

%% Synchronous call
list() ->
    gen_server:call(?MODULE, list).

create(Title) ->
    gen_server:call(?MODULE, {create, Title}).

join(Id, Name, Client) ->
    gen_server:call(?MODULE, {join, Id, Client, Name}).

%%%% backend
init([]) -> {ok, []}.

handle_call(list, _From, Games) ->
    {reply, [{Id, Title} || {Id, Title} <- Games], Games};
handle_call({create, Title}, _From, Games) ->
    {Mega, Seconds, _} = erlang:now(),
    Id = list_to_atom("g" ++ integer_to_list(Mega * 1000000 + Seconds)),
    {ok, Pid} = game:start_link(Id),
    Game = create_game(Title, 10, Pid),
    {reply, Game, [Game | Games]};
handle_call({join, Id, Client, Name}, _From, Games) ->
    io:format("games ~p~n", [[X || X <- Games, X#game.id == Id]]),
    [Game] = [X || X <- Games, X#game.id == Id],
    {reply, {ok, game:join(Game#game.pid, Client, Name)}, Games}.

handle_cast({leave, Player=#player{}}, Games) ->
    {noreply, lists:delete(Player, Games)}.

handle_info(Msg, Games) ->
    io:format("Unexpected message: ~p~n", [Msg]),
    {noreply, Games}.

terminate(normal, Games) ->
    ok.

code_change(_Old, Games, _Extra) ->
    {ok, Games}.

create_game(Id, Limit, Pid) ->
    #game{id=Id, players_limit=Limit, pid=Pid}.


Клиент, как его видит сервер.
  Скрытый текст
-module(client).
-author(neFormal).

-behaviour(gen_server).

-export([start_link/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
        code_change/3, terminate/2]).
-export([proc/2]).
-export([foo/0, bar/2]).

-record(state, {name, next_step, socket, game=null, ingame_id=0}).

-define(SOCK(Msg), {tcp, _Port, Msg}).

-include("cmd_macros.hrl").
% а в cmd_macros.hrl следующее:
%-define(LOBBY, 1).
%-define(CREATE, 2).
%-define(JOIN, 3).
%-define(LEAVE, 4).
%-define(EXIT, 100).
%-define(MOVE, 10).

start_link(Socket) ->
    gen_server:start_link(?MODULE, Socket, []).

init(Socket) ->
    gen_server:cast(self(), accept),
    {ok, #state{socket=Socket}}.

handle_call(E, _From, State) ->
    io:format("call ~p~n", [E]),
    {noreply, State}.

%% accept connection
handle_cast(accept, S = #state{socket=ListenSocket}) ->
    {ok, AcceptSocket} = gen_tcp:accept(ListenSocket),
    client_sup:start_socket(),
    inet:setopts(AcceptSocket, [{active, true}, {packet, 2}]),
    {noreply, S#state{socket=AcceptSocket, next_step=name}};
handle_cast({send, Data}, S = #state{socket=Socket}) ->
    io:format("msgpack:pack(Data): ~p~n", [msgpack:pack(Data)]),
    gen_tcp:send(Socket, msgpack:pack(Data)),
    {noreply, S};
handle_cast(E, S) ->
    io:format("cast ~p~n", [E]),
    {noreply, S}.

handle_info({tcp_closed, _}, S) ->
    {stop, normal, S};
handle_info(?SOCK(Msg), S = #state{socket=Socket}) ->
    io:format("info ~p~n", [Msg]),
    Data = parse_msg(Msg),
    NewState = process(Data, S),
    {noreply, NewState};
handle_info(?SOCK(Msg), S) ->
    io:format("info2 ~p~n", [Msg]),
    {noreply, S}.

code_change(_Old, State, _Extra) ->
    {ok, State}.

terminate(normal, State) ->
    ok.

send(Socket, Data) ->
    io:format("msgpack:pack(Data): ~p~n", [msgpack:pack(Data)]),
    gen_tcp:send(Socket, msgpack:pack(Data)),
    ok.

process(Data, State = #state{}) ->
    io:format("Data ~p~n", [Data]),
    {Msg, _} = Data,
    io:format("Message ~p~n", [Msg]),
    erlang:apply(client, proc, [Msg, State]).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% handlers
proc(?LOBBY, State) ->
    gen_server:cast(self(), {send, [?LOBBY, lobby:list()]}),
    State;
proc(?CREATE, State) ->
    lobby:create(<<"Foo">>),
    State;
proc([?JOIN, GameId, Name], State) ->
    {ok, {Game, IngameId}} = lobby:join(GameId, Name, self()),
    State#state{game = Game, ingame_id=IngameId};
proc([?MOVE, Id, Key, Value], State) ->
    game:move(State#state.game, self(), Id, Key, Value),
    State.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% readers
parse_msg(Bin) ->
    io:format("parse_msg ~p~n", [Bin]),
    io:format("1~n", []),
    NewBin = << (binary:list_to_bin(Bin))/binary >>,
    io:format("2~n", []),
    case msgpack:unpack(NewBin) of
        {more, _} -> parse_msg(NewBin);
        {Term, Remain} -> {Term, Remain}
    end.


Немного заметок по работе с клиентом.
1. строки надо приводить к бинарным, иначе при десериализации хз, что там было: список чисел или строчка.
2. берт может в атомы, мсгпак не может, но это не страшно, т.к. атомы пересылать — это несколько бредово имхо.
3. крайне привлекает фича эрланга в виде возможности указать, как передаётся пакет с данными. В моём случае это емнип `inet:setopts(AcceptSocket, [{active, true}, {packet, 2}])`. Т.е. 2 байта идут перед пакетом и указывают его размер. После разных написанных серваков на более других языках это выглядит адово вкусным.
4. обработка сообщений происходит через erlang:apply, хотя, наверное, можно и просто вызвать proc.
5. ну и отправка ответов идёт через постановку ещё одного сообщения. Sad but true.

Сама игра.

  Скрытый текст
-module(game).
-author(neFormal).
-behaviour(gen_server).

-export([start_link/1, join/3, leave/2, send/3, move/5, list/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

-record(player, {id, name, team=false, pid=false, object=null}).
-record(object, {id, type, x, y, z}).
-record(state, {players=[], objects=[], last_id=1}).

-include("cmd_macros.hrl").

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%% interface
start_link(Id) ->
    gen_server:start_link({local, Id}, ?MODULE, [], []).

join(Pid, Client, Name) ->
    gen_server:call(Pid, {join, Client, Name}).

leave(Pid, Client) ->
    gen_server:cast(Pid, {leave, Client}).

send(Pid, Client, Text) ->
    gen_server:cast(Pid, {send, Client, Text}).

move(Pid, Client, Id, Key, Value) ->
    gen_server:cast(Pid, {move, Client, Id, Key, Value}).

list(Pid) ->
    gen_server:call(Pid, list).


%%%% backend
init([]) -> {ok, #state{players=[], objects=[], last_id=1}}.

handle_call({join, Client, Name}, _From, S=#state{players=Players, objects=Objects, last_id=LastId}) ->
    Obj = create_object(0, LastId+1),
    Player = create_player(LastId+1, Name, Client, Obj),
    Joins = [?JOIN, LastId+1, Name, [Obj#object.id, Obj#object.x, Obj#object.y, Obj#object.z]],
    io:format("foreach ~p~n", [Joins]),
    gen_server:cast(Player#player.pid, {send, Joins}),
    lists:foreach(fun(P) ->
                          gen_server:cast(P#player.pid, {send, Joins}),
                          [#object{id=Id, type=T, x=X, y=Y, z=Z}] = [X || X <- Objects, X#object.id == P#player.id],
                          J = [?JOIN, P#player.id, P#player.name, [Id, X, Y, Z]],
                          gen_server:cast(Player#player.pid, {send, J})
                  end, Players),
    {reply, {self(), LastId+1}, S#state{players=[Player|Players], objects=[Obj|Objects], last_id=LastId+1}};
handle_call(list, _From, S) ->
    {reply, S}.


handle_cast({move, _C, Id, Key, Value}, S) ->
    Players = S#state.players,
    io:format("handle_cast({move: ~p~n", [Players]),
    Moves = [?MOVE, Id, Key, Value],
    lists:foreach(fun(Player) ->
                          io:format("foreach ~p~n", [Moves]),
                          gen_server:cast(Player#player.pid, {send, Moves})
                  end, Players),
    {noreply, S};
handle_cast({leave, Player}, S=#state{players=Players}) ->
    {noreply, S#state{players=lists:delete(Player, Players)}};
handle_cast({send, Client, Text}, S=#state{players=Players}) ->
    lists:foreach(fun({Player, _}) ->
                          client:send_text(Player#player.pid, Text)
                  end, Players),
    {noreply, S}.

handle_info(Msg, S=#state{}) ->
    io:format("Unexpected message: ~p~n", [Msg]),
    {noreply, S}.

terminate(normal, _S) ->
    ok.

code_change(_Old, S, _Extra) ->
    {ok, S}.

create_player(Id, Name, Pid, Obj) ->
    #player{id=Id, name=Name, pid=Pid, object=Obj}.

create_object(Type, Id) ->
    #object{id=Id, type=0, x=-107.575, y=26.6066, z=-0.490075}.


Всё довольно скучно. Хранит список клиентов и пересылает сообщение выбранным группам клиентов. Перемещение вообще просто транслируется с клиента без каких-либо проверок. Ну да мне это и не надо сейчас.



Клиент.


Panda3d использует питон для скриптинга. И под него для помощи в работе с сеткой был взят asyncore. Здесь вроде ещё бОльшая содомия в плане кода, т.к. было переделано из примера авторов движка. Они, кстати, не очень хорошо сделали, что текстуры оказались сложены в каком-то специфичном месте. Поэтому при копипасте у меня все текстуры пропали. Увы и ах.

Вот как-то так запускается игра и читается сетка.
  Скрытый текст
#!/usr/bin/python
#encoding: utf-8

import asyncore, socket, msgpack, struct

from world import Player, World

LOBBY = 1
CREATE = 2
JOIN = 3
LEAVE = 4
EXIT = 100
MOVE = 10

game = None

################################################################
class Client(asyncore.dispatcher):
    ''
    def __init__(self, address):
        asyncore.dispatcher.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect(address)
        self.ibuffer = []
        self.obuffer = []

    def handle_connect(self):
        print "handle_connect"
        pass

    def handle_close(self):
        self.close()

    def handle_read(self):
        size = struct.unpack('>H', self.recv(2))
        data = self.recv(size[0])
        self.ibuffer.append(msgpack.unpackb(data))

    def handle_write(self):
        if not len(self.obuffer):
            return
        sended = 0
        for data in self.obuffer:
            packed = msgpack.packb(data)
            self.send(struct.pack('>H', len(packed)))
            self.send(packed)
            sended += 1
        self.obuffer = self.obuffer[sended:]

################################################################
class Game:
    def __init__(self):
        global game
        game = self
        self.world = World(self)
        self.player = Player()

        avatarStartPos = self.world.environ.find("**/start_point").getPos()
        base.camera.setPos(avatarStartPos.getX(), avatarStartPos.getY()+20, 5)
        base.camera.lookAt(avatarStartPos)

        self.client = None
        self.init_net()

    def netTask(self, task):
        self.recv()

    def init_net(self):
        self.handlers = {
            JOIN : self.handle_join,
            MOVE : self.handle_move
            }
        
        self.client = Client(('localhost', 4000))
        import thread
        thread.start_new_thread(asyncore.loop, ())

    def send(self, data):
        print 'game.send'
        if self.client:
            self.client.obuffer.append(data)

    def recv(self):
        if not self.client or not len(self.client.ibuffer):
            return
        print 'recv'
        processed = map(lambda x: self.process(x), self.client.ibuffer)
        if len(processed):
            self.client.ibuffer = self.client.ibuffer[len(processed):]

    def process(self, msg):
        type = msg[0]
        data = msg[1:]
        print type, data
        if not self.handlers.has_key(type):
            return
        self.handlers[type](data)
        
    
    def handle_move(self, data):
        print 'move'
        id, key, value = data
        print id, key, value

        print self.world.keys.has_key(id)
        if self.world.keys.has_key(id):
            print 'set keys'
            self.world.keys[id][key] = value

    def handle_join(self, data):
        if hasattr(self, 'ingame_id'):
            print 'another player'
            player = Player()
            player.id = data[0]
            self.world.joined(player)
        else:
            print 'my player'
            self.ingame_id = data[0]
            self.player.id = data[0]
            avatar = self.world.joined(self.player)
            self.player.join(data[0], avatar)

################################################################
game = Game()
run()


Ну что тут можно хорошего сказать. Всё просто и глупо.
Создаются объекты, делается при запуске сразу коннект к серваку, обрыв фатален и требует перезапуска, протокол такой же с ручной вычиткой 2 байт перед пакетом, буфферы для чтения/записи в сетку.

А вот дальше почти копипаста из примера. Добавлены только хоткеи.

  Скрытый текст
#coding: utf-8

from math import pi, sin, cos
import direct.directbase.DirectStart
from panda3d.core import Point2,Point3,Vec3,Vec4,BitMask32
from panda3d.core import CollisionTraverser,CollisionNode
from panda3d.core import CollisionHandlerQueue,CollisionRay
from panda3d.core import Filename,AmbientLight,DirectionalLight
from panda3d.core import PandaNode,NodePath,Camera,TextNode
from direct.gui.OnscreenText import OnscreenText
from direct.showbase.DirectObject import DirectObject
from direct.task.Task import Task
from math import sin, cos, pi
from random import randint, choice, random
from direct.interval.FunctionInterval import Wait,Func
import cPickle, sys
from direct.showbase.ShowBase import ShowBase
from direct.actor.Actor import Actor

LOBBY = 1
CREATE = 2
JOIN = 3
LEAVE = 4
EXIT = 100
MOVE = 10


################################################################
class Player:
    ''
    def __init__(self, name='John Doe'):
        self.name = name
        self.id = -1

    def join(self, id, avatar):
        self.avatar = avatar
        self.id = id


################################################################
class World(DirectObject):
    ''
    def __init__(self, game):

        self.game = game

        base.disableMouse()

        self.environ = loader.loadModel("models/world")
        self.environ.reparentTo(render)
        self.environ.setPos(0,0,0)

       self.avatarStartPos = self.environ.find("**/start_point").getPos()
        print 'avatarStartPos:', self.avatarStartPos
        
        self.objects = {}

        self.keys = {}#{'left': 0, 'right': 0, 'forward': 0, 'backward': 0}
        self.accept('escape', sys.exit)
        
        self.accept('l', self.lobby, [])
        self.accept('c', self.create, [])
        self.accept('j', self.join, [])
        
        taskMgr.add(self.game.netTask, 'netTask')
        taskMgr.add(self.moveTask, "moveTask")
        self.isMoving = False
        
        # Create some lighting
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor(Vec4(.3, .3, .3, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection(Vec3(-5, -5, -5))
        directionalLight.setColor(Vec4(1, 1, 1, 1))
        directionalLight.setSpecularColor(Vec4(1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def lobby(self):
        self.game.send((LOBBY))
    def create(self):
        self.game.send((CREATE))
    def join(self):
        self.game.send((JOIN, "Foo", "Johnny"))

    def moveTask(self, task):
        self.game.recv()
        for id in self.keys:
            self.move(self.objects[id], self.keys[id])
        return task.cont

    def move(self, avatar, keys):
        startpos = avatar.getPos()

        if not keys['left']:
            avatar.setX(avatar, -25 * globalClock.getDt())
        if not keys['right']:
            avatar.setX(avatar, 25 * globalClock.getDt())
        if not keys['forward']:
            avatar.setY(avatar, 25 * globalClock.getDt())
        if not keys['backward']:
            avatar.setY(avatar, -25 * globalClock.getDt())
        
        if avatar != self.game.player.avatar:
            return

        if (keys['forward']!=0) or (keys['left']!=0) or (keys['right']!=0) or (keys['backward']!=0):
            if self.isMoving is False:
                avatar.loop('run')
                self.isMoving = True
        else:
            if self.isMoving:
                self.game.player.avatar.stop()
                self.game.player.avatar.pose('walk', 5)
                self.isMoving = False

        camvec = avatar.getPos() - base.camera.getPos()
        camvec.setZ(0)
        camdist = camvec.length()
        camvec.normalize()
        if (camdist > 20.0):
            base.camera.setPos(base.camera.getPos() + camvec*(camdist - 20))
            camdist = 20.0
        if (camdist < 10.0):
            base.camera.setPos(base.camera.getPos() - camvec*(10-camdist))
            camdist = 10.0

        base.camera.setPos(base.camera.getPos() + camvec*camdist)
        
        # check for collision
        self.cTrav.traverse(render)

        # Adjust ralph's Z coordinate.  If ralph's ray hit terrain,
        # update his Z. If it hit anything else, or didn't hit anything, put
        # him back where he was last frame.
        entries = []
        for i in range(self.avatarGroundHandler.getNumEntries()):
            entry = self.avatarGroundHandler.getEntry(i)
            entries.append(entry)
        entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                     x.getSurfacePoint(render).getZ()))
        if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"):
            avatar.setZ(entries[0].getSurfacePoint(render).getZ())
        else:
            avatar.setPos(startpos)

        # Keep the camera at one foot above the terrain,
        # or two feet above ralph, whichever is greater.
        entries = []
        for i in range(self.camGroundHandler.getNumEntries()):
            entry = self.camGroundHandler.getEntry(i)
            entries.append(entry)
        entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                     x.getSurfacePoint(render).getZ()))
        if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"):
            base.camera.setZ(entries[0].getSurfacePoint(render).getZ()+3.0)
        if (base.camera.getZ() < avatar.getZ() + 4.0):
            base.camera.setZ(avatar.getZ() + 4.0)

    def setKey(self, key, value):
        self.game.send((MOVE, self.game.player.id, key, value))
    
    def joined(self, player):
        ''
        avatarStartPos = self.environ.find("**/start_point").getPos()
        avatar = Actor('models/ralph', {'walk': 'models/ralph-walk', 'run': 'models/ralph-run'})
        avatar.setScale(.2)
        avatar.reparentTo(render)
        avatar.setPos(avatarStartPos)

        self.keys[player.id] = {'left': 0, 'right': 0, 'forward': 0, 'backward': 0}
        self.objects[player.id] = avatar
        avatar.reparentTo(render)

        # collision
        self.cTrav = CollisionTraverser()
        
        self.avatarGroundRay = CollisionRay()
        self.avatarGroundRay.setOrigin(0,0,1000)
        self.avatarGroundRay.setDirection(0,0,-1)
        self.avatarGroundCol = CollisionNode('avatarRay')
        self.avatarGroundCol.addSolid(self.avatarGroundRay)
        self.avatarGroundCol.setFromCollideMask(BitMask32.bit(0))
        self.avatarGroundCol.setIntoCollideMask(BitMask32.allOff())
        self.avatarGroundColNp = avatar.attachNewNode(self.avatarGroundCol)
        self.avatarGroundHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.avatarGroundColNp, self.avatarGroundHandler)
        
        self.camGroundRay = CollisionRay()
        self.camGroundRay.setOrigin(0,0,1000)
        self.camGroundRay.setDirection(0,0,-1)
        self.camGroundCol = CollisionNode('camRay')
        self.camGroundCol.addSolid(self.camGroundRay)
        self.camGroundCol.setFromCollideMask(BitMask32.bit(0))
        self.camGroundCol.setIntoCollideMask(BitMask32.allOff())
        self.camGroundColNp = base.camera.attachNewNode(self.camGroundCol)
        self.camGroundHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.camGroundColNp, self.camGroundHandler)
        
        self.accept('arrow_left', self.setKey, ['left', 1])
        self.accept('arrow_left-up', self.setKey, ['left', 0])
        self.accept('arrow_right', self.setKey, ['right', 1])
        self.accept('arrow_right-up', self.setKey, ['right', 0])
        self.accept('arrow_up', self.setKey, ['forward', 1])
        self.accept('arrow_up-up', self.setKey, ['forward', 0])
        self.accept('arrow_down', self.setKey, ['backward', 1])
        self.accept('arrow_down-up', self.setKey, ['backward', 0])
        
        return avatar


И оно даже как-то работает: http://rghost.ru/39143372/image.png
Можно подключаться и двигаться стрелками. Ну, всё, это победа.
Ох, блин, поворота нет, видно друг дружку по-очереди. Ну ладно, багу поправить — не реку перейти.
[throw-in]Теперь можно всё красиво и модно переписать на node.js[/throw-in]
...coding for chaos...
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.