2010年12月22日 星期三

將Roaming-Ralph.p3d嵌至網頁上的測試



以上是將Roaming-Ralph.p3d嵌至網頁上的測試。
請先安裝panda3d runtime程式
接受執行授權後按下綠色開始鈕。因為沒有申請正式的認證,所以只用測試的認證方式,沒有毒,請安心使用。

操作:方向鍵左右,方向鍵上。

2010年12月19日 星期日

發佈panda3d應用程式的獨立執行檔

Panda3D-1.7.0提供一種讓panda3d應用程式獨立執行的方式:
將應用程式包裝成p3d檔案,可在有安裝panda3d runtime環境下執行。

panda3d runtime甚至能執行嵌在網頁上的panda3d應用程式。例如官方網站的Airblade

p3d檔的包裝方式可參考網頁。不過,在Panda3D-1.7.0中,首先得從此下載缺失的檔案packp3d.p3d,放在panda3d的bin資料夾內(參考討論串)。並且必須安裝panda3d runtime程式

用前一個範例Roaming-Ralph做p3d包裝測試。照參考討論串所描述,在命令模式下鍵入:
D:\Panda3D-1.7.0\bin\packp3d.exe -o Roaming-Ralph.p3d -d D:\Panda3D-1.7.0\samples\A_Roaming-Ralph_OO
(packp3d -o 包裝輸出檔名.p3d -d 包裝來源資料夾路徑)
指定執行起始檔名: -m mystart.py,沒指定則預設為main.py。

等一段時間後,packp3d即將目標資料夾包裝成獨立的p3d檔案。(下載Roaming-Ralph.p3d
點擊此檔即可開啟panda3d應用程式。

發佈時僅需要此獨立的p3d檔案,並且在執行環境中安裝panda3d runtime


以下是將Roaming-Ralph.p3d嵌至網頁上的測試。接受執行授權後按下綠色開始鈕。請先安裝panda3d runtime程式

2010年12月18日 星期六

Case Study:Roaming-Ralph

簡介

Roaming-Ralph演示一個在場景(地形)中行走的角色,以第三視角觀看。我嘗試將原本的範例程式改寫得更容易理解。這裡傾向表達清楚,並沒有對效能優化多做調整。原始檔可由此下載
操作方式:方向鍵左右表示左右轉,方向鍵上表示前進。


此範例將會用到:
- 起始panda3d
- 接收鍵盤按鍵事件
- 3d模型的位置移動與面向設定
- 碰撞偵測的設定
- 碰裝物件的擷取
- 攝影機位置與面向設定

在這個範例中,共有四種物件。
1. 控制器 -- 接收鍵盤事件的的控制器。
2. 角色 -- 根據鍵盤命令移動的角色。
3. 場景 -- 場景提供角色與地面的碰撞偵測,讓角色能走在地面上,並且不能進入障礙物如樹,石頭中。
4. 攝影機 -- 跟著角色移動的攝影機。

最後用一個世界物件,將上述四種物件裝起來。
因此我將檔案分成--
main.py -- 執行起始。
_define.py -- 一些定義,主要是控制器的命令定義。
World.py -- 世界類別。
Controller.py -- 控制器類別。
Avatar.py -- 角色類別。
Environment.py -- 場景類別。
CameraController.py -- 攝影機類別。

以下簡單描述各檔案的內容。


世界類別

main.py檔案很單純的僅是產生一個世界類別,然後開始panda3d的main loop。
import direct.directbase.DirectStart
from World import World

w = World()

run()

世界類別,World.py。先載入角色與場景的3d模型(函數_model_load中,呼叫_avatar_model_get與 _environment_model_get函數),並賦予給角色物件與場景物件。設定好控制器(_controller),角色(_avatar),場景(_environment),攝影機(_camera_ctrl)等物件後。將每個frame更新的函數(_main_loop)掛上 taskMgr。
每個frame的更新函數(_main_loop)會依序呼叫角色的更新函數,場景的更新函數,最後是攝影機更新函數。
角色更新函數主要是從控制器物件獲取目前命令狀態,並依此做角色的3d模型位置的y值與面向更新。
場景更新函數主要是讓角色能站在地面上,依據碰撞偵測的反應更新角色模型位置的z值。
最後,攝影機取得角色的位置,並依此更新攝影機的位置與面向。

角色更新與場景更新函數可這麼看:角色更新函數依據輸入命令與自身狀態,給出理論的角色行為。場景更新函數依據角色與場景間的關係,修正理論的角色行為,得到實際上的角色行為。如此範例中,角色更新函數依據輸入命令前進,但場景更新函數因為角色將會進入障礙物而不讓角色前進,修正角色的位置為改變前的位置。
from direct.actor.Actor import Actor
from Controller import Controller
from Avatar import Avatar
from Environment import Environment
from CameraController import CameraController

def _environment_model_get():
    environment = loader.loadModel("models/world")
    environment.reparentTo(render)
    environment.setPos(0,0,0)
    return environment

def _avatar_model_get():
    avatar = Actor("models/ralph",
                    {"run":"models/ralph-run",
                     "walk":"models/ralph-walk"})
    avatar.reparentTo(render)
    avatar.setScale(.2)
    return avatar

class World:
    def __init__(self):
        self._model_load()

        self._controller = Controller()
        self._avatar = Avatar(self._avatar_model, self._controller)
        self._environment = Environment(self._environment_model, self._avatar)
        self._camera_ctrl = CameraController(self._avatar)

        self._main_loop_start()

    def _model_load(self):
        self._environment_model = _environment_model_get()
        self._avatar_model = _avatar_model_get()

        avatar_start_pos = self._environment_model.find("**/start_point").getPos()
        self._avatar_model.setPos(avatar_start_pos)

    def _main_loop_start(self):
        taskMgr.add(self._main_loop,"Main Loop")

    def _main_loop(self, task):
        self._avatar.update()
        self._environment.update()
        self._camera_ctrl.update()
        return task.cont

控制器類別

控制器類別,Controller.py。內含目前命令狀態(key_state)。在接收鍵盤事件後,更新目前命令狀態值(函數setKey)。在這個範例中僅有三種命令--左轉,右轉,與前進。
鍵盤事件僅包含按鍵按下與按鍵放開,例如"arrow_left"表示左箭頭按鍵按下的事件,"arrow_left-up"表示左箭頭按鍵放開的事件。因此,按鍵持續按著的這個動作就由key_state保存,在按下後設定對應的key_state命令狀態為1,與放開後設定對應的key_state命令狀態為0。
from direct.showbase.DirectObject import DirectObject
from _define import _LEFT, _RIGHT, _FORWARD

class Controller(DirectObject):
    def __init__(self):
        self.key_state = {_LEFT:0, _RIGHT:0, _FORWARD:0}
        self._keyborad_event_setup()

    def setKey(self, key, value):
        self.key_state[key] = value

    def _keyborad_event_setup(self):
        self.accept("arrow_left", self.setKey, [_LEFT,1])
        self.accept("arrow_right", self.setKey, [_RIGHT,1])
        self.accept("arrow_up", self.setKey, [_FORWARD,1])
        self.accept("arrow_left-up", self.setKey, [_LEFT,0])
        self.accept("arrow_right-up", self.setKey, [_RIGHT,0])
        self.accept("arrow_up-up", self.setKey, [_FORWARD,0])

    def state_get(self):
        ret = self.key_state
        return ret


角色類別

角色類別,Avatar.py。在每個frame時,從控制器取得目前命令狀態(函數_cmd_get),藉此更新角色3d模型的y值與面向(函數_pos_update),並更新角色3d模型的動畫狀態(函數_animation_update)。函數pos_recover用來恢復角色3d模型改變前的位置,於之後的場景類別中使用。
from _define import _LEFT, _RIGHT, _FORWARD

class Avatar:
    def __init__(self, model, controller):
        self.model = model
        self._isMoving = False
        self._cmd_get = controller.state_get

    def update(self):
        cmd_state = self._cmd_get()
        cmd_forward = cmd_state[_FORWARD]
        cmd_left = cmd_state[_LEFT] - cmd_state[_RIGHT]

        self._pos_update(cmd_forward, cmd_left)
        self._animation_update(cmd_forward, cmd_left)

    def _pos_update(self, cmd_forward, cmd_left):
        self._old_pos = self.model.getPos()
        if cmd_forward:
            self.model.setY(self.model, -25 * globalClock.getDt())
        if cmd_left:
            self.model.setH(self.model.getH() + (cmd_left * 300 * globalClock.getDt()))

    def _animation_update(self, cmd_forward, cmd_left):
        if (cmd_forward or cmd_left):
            if (not self._isMoving):
                self.model.loop("run")
                self._isMoving = True
        else:
            if self._isMoving:
                self.model.stop()
                self.model.pose("walk",5)
                self._isMoving = False

    def pos_recover(self):
        self.model.setPos(self._old_pos)


場景類別

場景類別,Environment.py。設定角色的碰撞(函數_collision_setup),於每個frame更新時檢查碰撞事件(函數 _collision_object_list_get),並藉此更新角色3d模型位置的z值(函數_collision_handle),讓角色能隨著地形起伏改變高度,達到"站在地上"的視覺效果。當角色3d模型"進入"場景中的石頭或樹木等障礙物時,恢復到角色移動前的位置(角色類別的函數 pos_recover),如此角色就不會穿越障礙物了。
from panda3d.core import CollisionTraverser,CollisionNode
from panda3d.core import CollisionHandlerQueue,CollisionRay
from panda3d.core import BitMask32

class Environment:
    def __init__(self, model, avatar):
        self.model = model
        self._avatar = avatar
        self._avatar_model = avatar.model

        base.win.setClearColor((0,0,0,1))
        self._collision_setup()

    def _collision_setup(self):
        self.cTrav = CollisionTraverser()
        self._avatar_GroundRay = CollisionRay()
        self._avatar_GroundRay.setOrigin(0,0,1000)
        self._avatar_GroundRay.setDirection(0,0,-1)
        self._avatar_GroundCol = CollisionNode('_avatar_Ray')
        self._avatar_GroundCol.addSolid(self._avatar_GroundRay)
        self._avatar_GroundCol.setFromCollideMask(BitMask32.bit(0))
        self._avatar_GroundCol.setIntoCollideMask(BitMask32.allOff())
        self._avatar_GroundColNp = self._avatar_model.attachNewNode(self._avatar_GroundCol)
        self._avatar_GroundHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self._avatar_GroundColNp, self._avatar_GroundHandler)

    def update(self):
        entries = self._collision_object_list_get()
        self._collision_handle(entries)

    def _collision_object_list_get(self):
        self.cTrav.traverse(render)
        entries = [self._avatar_GroundHandler.getEntry(i)
                   for i in range(self._avatar_GroundHandler.getNumEntries())]
        entries.sort(key = lambda x: x.getSurfacePoint(render).getZ())
        return entries

    def _collision_handle(self, obj_list):
        if (len(obj_list)>0) and (obj_list[0].getIntoNode().getName() == "terrain"):
            self._avatar_model.setZ(obj_list[0].getSurfacePoint(render).getZ())
        else:
            self._avatar.pos_recover()


攝影機類別

攝影機類別,CameraController.py。這裡只是很簡單的設定攝影機的位置與面向,讓角色能以第三人視角表現。
class CameraController:
    def __init__(self, avatar):
        self._avatar_model = avatar.model

        base.disableMouse()

    def update(self):
        base.camera.setPos(self._avatar_model.getX(),
                           self._avatar_model.getY()+10,
                           self._avatar_model.getZ()+2)
        base.camera.lookAt(self._avatar_model)

2010年12月12日 星期日

Q&A -- 使用__dict__["__setattr__"]取代現有的類別函數

Q:

操作物件內的__dict__字典相當於操作物件內的屬性存取. 雖然仍有些不同.
obj.__dict__[name] = value # obj.name = value

然而以 obj.__dict__["__setattr__"] = new_fn 來取代現有的 __setattr__ 時
遇到一個問題(使用python2.6):

class aKlass:
    var = 0
    def a_fn(self):
        print "aKlass.a_fn"
    def __setattr__(self, name, value):
        print "aKlass.__setattr__",name,value
                                                                                
def new_a_fn(self):
    print "new_a_fn"
def new_setattr_fn(self,name,value):
    print "new_setattr_fn",name,value

aKlass內含兩個函數, a_fn, __setattr__.
另準備兩個對應的函數new_a_fn, new_setattr_fn
希望使用aKlass.__dict__[ ]的方式取代 --

aObj = aKlass()
aObj.a_fn()
aKlass.__dict__["a_fn"] = new_a_fn
aObj.a_fn()
aObj.var = 1
aKlass.__dict__["__setattr__"] = new_setattr_fn
aObj.var = 1


執行結果:

aKlass.a_fn
new_a_fn
aKlass.__setattr__ var 1
aKlass.__setattr__ var 1


aKlass.__dict__["a_fn"] = new_a_fn 的確用new_a_fn取代了原先的a_fn.
但似乎 aKlass.__dict__["__setattr__"] = new_setattr_fn 沒有達到預期的效果.
不知原因為何?

(雖然使用 setattr(aKlass,"__setattr__",new_setattr_fn) 就可以取代原函數了.)



A:

你測試的兩個 case 並不算是全等的,如果讓兩者在做法更接近,應該是:
aKlass.__dict__["a_fn"] = new_a_fn
aObj.a_fn()
aKlass.__dict__["__setattr__"] = new_setattr_fn
aObj.__setattr__('var', 1)

接下來回到為什麼修改 aKlass.__dict__ 後,在如此的 statement:
aObj.var = 1
上看不到作用?

1. 這只會發生在 old-style class。
(new-style class 的 __dict__ 則是 immutable,實際上是個 proxy)

2. aKlass object(不是指 aObj)除了 __dict__ 裡有 reference 指涉原本在
constructing aKlass 過程中所建立的 __setattr__ function(這個 function
是指 aKlass.__setattr__.im_func),aKlass object 本身也有一個欄位存著
該 function 的位址。(可以看看 Python 安裝後一併附的 classobject.h)

我把 CPython 2.6.5 的 classobject.h 部分內容節錄在此:
typedef struct {
    PyObject_HEAD
    PyObject    *cl_bases;      /* A tuple of class objects */
    PyObject    *cl_dict;       /* A dictionary */
    PyObject    *cl_name;       /* A string */
    /* The following three are functions or NULL */
    PyObject    *cl_getattr;
    PyObject    *cl_setattr;
    PyObject    *cl_delattr;
} PyClassObject;

aKlass 在記憶體裡的結構會分別有 __getattr__/__setattr__/__delattr__
的位址(if any)。
當執行過此 statement:

aKlass.__dict__['__setattr__'] = new_setattr

只變更了 cl_dict 所指涉的 dict 的狀態,並沒有變更 cl_setattr 指向另一個
function,而 aObj.var = 1 statement 會直接使用 cl_setattr 的值(一種優化,
省了 dictionary lookup 動作),所以 aObj.var = 1 還是調用了原來的
__setattr__ function。

如果你使用下列的 statement(任一)來變更 __setattr__ 屬性:
aKlass.__setattr__ = new_setattr_fn
setattr(aKlass, '__setattr__', new_setattr_fn)

則會變更 cl_setattr 欄位指向 new_setattr_fn,所以 aObj.var = 1
有預期中的表現。

接下來我要透過一些手法來變更 aKlass 在記憶體中其 cl_setattr 欄位的值,
看看有什麼效果。
* 有心一試的人請在 console mode 執行 python REPL(interpreter),不要
使用 IDLE 或是 PythonWin Editor 等 IDE 環境。

沿用 aKlass/new_a_fn/new_setattr_fn 的定義。
接下來 import ctypes 套件(已內建於 Python 2.5+)

from ctypes import *
a = aKlass()
wrapper = py_object(aKlass)
p = cast(addressof(wrapper), POINTER(POINTER(c_uint32))).contents
                                                                                
print id(aKlass.__setattr__.im_func) # address of original __setattr__
                                     # function
for x in xrange(10):
    print x, p[x]

執行完以上的程式碼後,大概可以看到類似如下的 output:
12325616
0 3
1 505362952
2 11210800
3 11416608
4 12313696
5 0
6 12325616
7 0
8 7
9 505362336

這表示 p[6] 是 cl_setattr 欄位(p[5] 是 cl_getattr,p[7] 是 cl_delattr,
兩者皆是 0,因為 aKlass 沒有定義 __getattr__, __delattr__)。

old_setattr_addr = p[6]    # 記下原 __setattr__ 的位址
p[6] = id(new_setattr_fn)
a.var = 1                  # see output
p[6] = old_setattr_addr
a.var = 2                  # see output

以上幾個 statement 的輸出應該如下:

new_setattr_fn var 1
aKlass.__setattr__ var 2

Q&A -- 如何用程式判斷某函數是否含有yield?

Q:

如何能在不執行某函數的狀況下, 以程式判斷此函數含有yield?
(即此函數可以成為iterator)



A:

inspect.isgeneratorfunction



Example:

def a_iter_fn():
    yield 1

def a_fn():
    return 1

from inspect import isgeneratorfunction

print isgeneratorfunction(a_iter_fn)
print isgeneratorfunction(a_fn)

Result:

True
False

2010年10月17日 星期日

pynguin -- python下的海龜繪圖

簡介


海龜繪圖(logo)是一種學習程式語言概念很好的入門,網路上可以找到許多種語言實作的logo。這裡我介紹一個網路上找到的,以python+ Qt4建構的logo環境。除了可以學習基礎的程式概念,還能順便學python。


海龜繪圖

海龜繪圖(logo)

假想一隻帶著畫筆的海龜可以接受簡單的命令,例如向前走100步,或者左轉30度。通過對這隻海龜發送命令,可以讓它繪製出較為複雜的圖形,例如正方形,三角形,圓等。

海龜的移動相對於它本身所在的位置。例如,命令"左 90"意味著讓海龜左轉90度,學生可以站在海龜的角度來思考它將如何執行命令,這使得程序設計更加形象化,也更易於理解。

pynguin

於python下已有自帶的Tk版本海龜繪圖環境--turtle — Turtle graphics for Tk
這個作者製做了另一個基於python + Qt4 gui的海龜環境(雖然說預設顯示是隻企鵝)--pynguin。以下是企鵝的表現截圖:Screenshots

但由於需要安裝python與Qt4才能執行,對一般使用者來說有些麻煩。於是我將它包裝成獨立執行檔。可在此下載
解壓縮後執行pynguin.exe即可打開介面。



開啟範例

初 學者可先從作者提供的範例開始玩。從File -> Example -> ... 可以看到內附的例子。這裡選擇第2個例子。載入後,函式的程式碼會顯示在右上的黑色框框內。按右下的"Test"按鈕,右下的互動式介面會顯示將 要被執行的命令。在右下黑框內按下enter即可開始執行。Ctrl-C可中止執行。


命令簡述

可對企鵝下的命令如下:

# 相對坐標
forward(n)       # 前進n個像素距離。等同於fd(n)。
backward(n)      # 後退n個像素距離。等同於bk(n)。
left(n)          # 左轉角度n。等同於lt(n)。
right(n)         # 右轉角度n。等同於rt(n)。

circle(n)        # 畫圓, 半徑n個像素距離
circle(n, True)  # 以目前位置為圓心畫圓, 半徑n個像素距離
# 絕對坐標
goto(x,y)        # 移動至(點x, 點y)

reset()          # 清除繪圖區

更多說明在 doc\worksheets\

在右下黑框內輸入
fd(100)
lt(90)
fd(100)
lt(90)
即可看到企鵝隨著指令移動。


參考

python語法介紹--Python Handbook

更深入的海龜繪圖應用--碎形幾何參考網站:碎形幾何內涵與 Logo 程式繪圖

2010年10月5日 星期二

在視窗中嵌入shell介面

shell介面可以以文字形式,以直接鍵入函數與參數的方式控制應用程式,甚至執行一段script。

wxPython提供一個shell的應用程式,稱做PyShell,可以在
Python26\Lib\site-packages\wx-2.8-msw-unicode\wx\py目錄下
執行PyShell.py。

目錄下的shell.py中的Shell類別是此shell程式的核心,放到應用程式中即是一個現成的shell介面。此shell介面還支援自動完成與函數提示。


以下是簡單的嵌入範例,將Shell類別置入一個wx.Panel中。
"""
ShellPanel provides a shell interface in a panel.

# put shell in a frame
self.myShellPanel = ShellPanel(objDict = {'win':self}, parent = self)
"""

import wx
import wx.py as  py

class ShellPanel(wx.Panel):
    def __init__(self, objDict, *args, **kwds):
        """\
ShellPanel(objectDict, parent, ...)
    Create a Shell panel and put some object into this shell;
    Assign "myHelpInfo" object for default help text."""
        # format of objdict: { Name:object }

        kwds["style"] = wx.TAB_TRAVERSAL
        wx.Panel.__init__(self, *args, **kwds)

        self.objectDict = objDict
        self.myShell = py.shell.Shell(self, -1, locals = self.objectDict )

        sizer_Shell = wx.BoxSizer(wx.HORIZONTAL)
        sizer_Shell.Add(self.myShell, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_Shell)

self.myShell = py.shell.Shell(self, -1, locals = objDict)
參數locals接收一個{名稱字串:物件}的字典,可將物件以對應的名稱字串在shell介面中操作。如果將應用程式(視窗)本身傳入,即可在shell介面中控制視窗,如上例將視窗物件以"win"命名,並在shell介面下輸入"win.SetTitle('Hello World')",便可改變視窗Title顯示。

此shell介面比起python內建的IDLE仍有些缺點。如下程式碼:
from time import sleep
for i in range(10): sleep(1)
無法如同在IDLE中,以鍵盤方式中斷。
(參考網頁:命令行的 Python shell 保留了處理鍵盤中斷( Ctrl +C)的能力...)

更多的Shell類別建構參數使用,請參考shell.py原始碼。

2010年10月3日 星期日

在py2exe包裝後的執行環境下執行python檔案

py2exe可以將python檔案包裝成獨立執行檔,便可在不安裝python interprter的環境下獨立執行。
然而每次執行py2exe包裝總得花上許多時間,如果能將時常變動的程式包裝成獨立的python檔案,並且在每次修改後都能直接加入包裝內,便可減少頻繁修改後的再次包裝,甚至此經常修改的部分可以讓使用者自行修改,以滿足實際需求。

py2exe包裝後,將python檔案放入library.zip內,可以在此找到原先的模組結構。雖然它將python文字檔編譯成pyc檔,但仍然可以將原先的文字檔放入並刪除pyc檔,這時候便會執行放入的文字檔。
 但在library.zip的文字檔無法直接打開修改。此外,跟著原先的模組取代,對使用者而言似乎不是這麼直覺,如果能在一個單純的資料夾下的固定檔案放置檔案,應該可以更容易使用。

如果將文字檔直接擺在library.zip之外的地方,會遇到無法找到模組的錯誤。這是因為py2exe包裝之後,sys.path內僅看得到"library.zip"。加入模組路徑之後即可解決。以下是簡單的程式碼示意,將根目錄加入sys.path。

        import sys
        # dist mode
        if len(sys.path)==1:
            path = sys.path[0]
            distDir = "library.zip"
            index = -1*len(distDir)
            if path[index:] == distDir:
                rootPath = path[:index]
                sys.path.append(rootPath)

2010年7月29日 星期四

Cube -- a Game-loader

近幾個月來,我嘗試以panda3d來做一個簡易的game-loader,取名作"Cube",目的是想讓使用panda3d做單機/p2p多人連線遊戲更容易些。那很像是custom map 運行在WarCraft3 上,或許稱做 map-loader 會更容易被理解。Cube 將包含一個選單介面,以及 p2p連線時需要的 login 與 lobby 介面,並提供一個p2p的同步機制。

目前正在以一個簡單範例 map 來驗證p2p同步機制的正確性,map 素材取自panda3d的asteroids 範例程式

Cube 仍尚未完成,原始碼可以在Cube project上看到。以下是一些Cube 的截圖:


1. main menu


















2. single player (目前尚未進一步處理)


















3. login


















4. lobby  (server)


















5.  count down

















6. a sample map
















A testing of synchronization betweent server and client.


2010年2月17日 星期三

聊天室練習 - VI. GUI

Panda3D提供一些基本2d gui模組,稱做Direct GUI,參考這裡
Login 畫面(LoginFrame.py)是由DirectLabel、DirectEntry、DirectRadioButton、 DirectButton等元件組成,如下圖所示。在需要取得文字輸入之處使用DirectEntry,如輸入使用者暱稱以及輸入server ip 位址。需要多選一之處使用DirectRadioButton,如選擇開啟聊天室或加入。需要取得按鈕動作之處使用DirectButton,如按下 Login按鈕表示以這些輸入資料進行連線動作。而DirectLabel用在表示提示文字。其他gui介紹請參考panda3d網頁。



ChatRoom 畫面(ChatRoomFrame.py)是由DirectEntry、DirectButton與自定義的ScrolledTextFrame組成,如 下圖所示。按鈕Host IP按下後會跳出個對話框,用以顯示server端ip位址。聊天室對話的文字輸入以DirectEntry處理。

顯 示聊天室文字與顯示目前聊天室成員清單,需要可捲動的文字顯示gui,雖然可以由DirectScrolledList內嵌DirectLabel實作, 但處理捲動的部份個人覺得不夠好,因此嘗試以DirectScrolledFrame內嵌OnscreenText來實作。 DirectScrolledFrame的捲動部份比較類似一般在視窗上看到的捲動棒(scrolled bar),而OnscreenText也提供appendText函數用來新增文字。美中不足的,DirectScrolledFrame並不會自動調整 捲動機制以符合內嵌物件大小,因此必須在每次OnscreenText因為新增文字而使大小改變時,重新設定DirectScrolledFrame捲動 大小(ScrolledTextFrame.py,第68行。 ScrolledTextFrame.resetCanvasSizeByTextBoard函數)。其中,mySetCanvasSize函數指向 ScrolledTextFrame.guiItem.setVirtualFrame函數,setVirtualFrame可用來設定 DirectScrolledFrame的內嵌物件大小,以改變捲動棒行為。



Direct GUI功能有限,行為比較複雜的gui元件可嘗試用基本元件組合,例如此練習中的可捲動的文字顯示組合自DirectScrolledFrame與 OnscreenText。另外,有些函數並不直接提供,藏在gui 內部物件的函數中,如DirectScrolledFrame 中用以改變內嵌物件大小的函數setVirtualFrame 藏在其內部物件guiItem 中。因此,翻翻原始碼(py檔)也許可以找到些gui 元件不直接提供的功能。
再者,Direct GUI對於gui元件擺設位置,大小的設定不是很直覺,它是以視窗大小的寬當成1,如像素800x600視窗,大小是(1.333, 1),因此像素300x300的gui 物件大小是(0.5, 0.5)。某些物件的擺設位置(0,0,0) 表示視窗正中央,而某些物件擺設位置的(0,0,0) 卻是相對另外一個物件的相對位置。

除了Direct GUI,也有其他使用者貢獻的gui模組,如TreeGUI,甚至還提供可拖曳的視窗。



----

  1. 操作方式
  2. 程式架構
  3. 網路設定、接收與傳送
  4. Server - Client 間的傳輸協定
  5. 傳輸命令格式
  6. GUI
  7. 中文輸入與顯示
  8. 事件驅動

聊天室練習 - VII. 中文輸入與顯示

在聊天室練習中,尚未找到支援中文輸入的方法,僅完成中文顯示。
要顯示中文,必須在Panda3D執行環境裡指定含有中文字型的字集。某些 Direct GUI,如OnscreenText,可設定gui 元件個別的字集。或是在Panda3D設定檔中指定預設字集(UnicodeDefine.py,第15行),如此所有字型相關的顯示都使用此預設字集。
在python原始檔中嵌入中文字,得做些編碼設定,見ChatRoomMain.py,第一行。
# -*- coding: utf-8 -*-
設定此檔案以utf-8方式編碼。



----

  1. 操作方式
  2. 程式架構
  3. 網路設定、接收與傳送
  4. Server - Client 間的傳輸協定
  5. 傳輸命令格式
  6. GUI
  7. 中文輸入與顯示
  8. 事件驅動

聊天室練習 - VIII. 事件驅動

事件驅動的優缺點於先前的文章已經描述過,在此就不再重複。僅以一個簡單的測試來檢驗啟動事件與事件執行的順序。
在ChatRoomMain.py,第130行之後插入
print 'return to CommandParser'
另外,在ChatRoomFrame.py,第81行之後插入
print 'enter event_TEXTDISPLAY handle'

當輸入對談訊息後,ChatRoomMain.CommandParser啟動事件event_TEXTDISPLAY,對應的事件執行在ChatRoomFrame.CmdTextDisplayHandle。列印出的訊息如下:
enter event_TEXTDISPLAY handle
return to CommandParser
enter event_TEXTDISPLAY handle
return to CommandParser
enter event_TEXTDISPLAY handle
return to CommandParser
enter event_TEXTDISPLAY handle
return to CommandParser
enter event_TEXTDISPLAY handle
return to CommandParser

從列印出的訊息可以看出,每次啟動事件(ChatRoomMain.py,第130行)後,總是先處理事件執行(ChatRoomFrame.py,第81 行),執行完後才再回到啟動事件處繼續執行(ChatRoomMain.py,第131行,print 'return to CommandParser')。猜測啟動事件後的事件執行並不會獨立出一個thread來處理,而是如同函數呼叫般直接跳至call back function。因此ChatRoomMain.py,第130行
messenger.send(event_TEXTDISPLAY,[cmdItem['i'], cmdItem['param']])
可以看成
self.ChatRoomFrame.CmdTextDisplayHandle(cmdItem['i'], cmdItem['param'])

在聊天室練習中,由於環境較單純,使用事件驅動似乎沒得到明顯的好處。
假設一個遊戲過程中,不同階段都有不同的處理對談文字顯示的函數。在不使用事件驅動的方式下,呼叫端(例如此練習中的CommandParser)必須要以階段來判斷該執行哪個文字顯示函數,並且當文字顯示函數更動名稱時,例如改變函數名稱或是換到不同類別內,呼叫端也要跟著更新。
但若使用事件驅動的方式,呼叫文字顯示與執行文字顯示的函數間以一個事件名稱(例如此練習中的event_TEXTDISPLAY)當介面連接,只要執行文字顯示的函數綁到此事件名稱的call back function,就可以在不更動呼叫端程式碼的情形下改變執行文字顯示的函數。因此,使用事件驅動應該可以減少程式維護上的成本。

為了避免事件名稱的衝突,在此練習中統一將事件名稱定義在CommandInputLib.py的最後。



----
  1. 操作方式
  2. 程式架構
  3. 網路設定、接收與傳送
  4. Server - Client 間的傳輸協定
  5. 傳輸命令格式
  6. GUI
  7. 中文輸入與顯示
  8. 事件驅動

聊天室練習 - III. 網路設定、接收與傳送

Panda3D的網路設定參考這裡

NetworkLib.py 中,函數NetworkSetupServer與NetworkSetupClient分別設定連線為server或是client端。連線成功後,將處理網路封包讀取與檢查連線狀態的task加至系統中,server端還會多加入"聽"client連結的task。
NetworkConnectionTarget 這個list保存連線的對象。網路封包讀取時,用以標明是哪個對象送來的封包(NetworkLib.py,第229行)。與網路封包傳送時,用以表示送 給哪個對象的封包(NetworkLib.py,第175行,第189行)。檢查連線狀態與移除連線時也用於表示對象(NetworkLib.py,第 248行,第256,257行)。server端的NetworkConnectionTarget依序存放連進來的client。client端的 NetworkConnectionTarget僅保存server為其唯一的連線對象。

網路封包傳送的運作,只要 呼叫NetworkDataObjSend或NetworkDataObjSendAll函數。在這個練習中,NetworkDataObjSend是設 計給client,以傳送給它唯一的對象,NetworkDataObjSendAll是設計給server,以傳送封包給它所有的client對象。設計的原因在稍後討論Server - Client 間的傳輸協定時在說明。

雖然panda3D支援將整數,浮點數,字串等形態物件以個別方式填入封包,參考這裡, 如整數就依byte需求分成getInt8,getInt16,getInt32,getInt64等處理函數。針對不同需求使用對應函數應該會得到較小的封包,但讀取時需要依序呼叫對應的處理函數,相當於寫一個parser來還原封包內容,當傳輸內容或順序更改時,對應的還原封包函數也得變動。且這些支 援的型態並不包含list或dictionary等python內建型態。使用此方式包裝資料有取得較小封包的優點,但代價是較複雜的還原封包內容的方式。

這個練習中並沒有使用上述方式,而是以python提供的cPickle.dumps函數,將python物件打包成字串,再將此字串以zlib.compress壓縮後傳送。還原封包的流程正好相反,將讀取的封包內容字串以zlib.decompress解壓 縮,再用cPickle.loads還原成物件。這樣雖然會產生較大的封包,但在處理資料填入封包與還原的方式較簡單,並且可以傳送任何python物件,於新一版panda3D 1.7中,甚至可以傳送panda3d內的自訂物件如point3等。

網 路封包讀取的操作為:每個frame執行tskReaderPolling這個task,檢查是否有封包進入。當封包進入時,以前一段的描述方式從封包擷取資料,最後呼叫封包處理函數NetworkDataObjGet。NetworkDataObjGet為一空函數,預留給真正做資料處理的函數覆載 (ChatRoomMain.py,第40行)

下圖為資料由接收端,經過網路至傳送端,傳送端解開資料並處理的示意流程。




----

  1. 操作方式
  2. 程式架構
  3. 網路設定、接收與傳送
  4. Server - Client 間的傳輸協定
  5. 傳輸命令格式
  6. GUI
  7. 中文輸入與顯示
  8. 事件驅動

聊天室練習 - IV. Server - Client 間的傳輸協定

server 端與client 端間的傳輸協定的目標為執行命令的同步,在此的"執行命令的同步"是指所有參予聊天室的成員,其對話文字顯示順序相同。實作方式為,server端蒐集所有的執行命令,再將這些命令傳送給每個client,如此server 與每個client 都有相同的執行命令。因此client要做的是,把自己的執行命令傳給server。server 端與client 端間的傳輸如下圖所示。
雖然上圖的流程達成了每個參予者的執行命令同步,但還不夠完全。從命令輸入到命令執行這段延遲時間(上圖的delayT),對於每個參予者而言不保證相等。在這個練習中,程式於delayT之間不做任何事情,因此看不出不等長的delayT造成的影響。但如果在即時性的環境中,每一個frame都有可能使狀態改變,假設一個遊戲中的每個frame,參予者都會不斷向前移動,而命令輸入用以改變參予者的移動方向,若每個參與者的delayT不等長,則每個參予者將不會得到完全一樣的執行結果。

因此在需要即時性的環境中,必須要保證執行命令不只在順序上同步,也要在時間上同步。實作方式也許可以讓server 無論有無收到參予者的執行命令,都要按時傳送執行命令串給client 端,client 端要在協定的時間內取得server端傳來的執行命令,如果沒收到server端的命令串則凍結即時環境的運行直到收到命令串。如此所有參與者能在相同delayT時,執行命令串。接著client 再傳個"命令已經正確被執行"的回覆給server,server 收到所有client的回覆後,才算完成這一輪的傳輸,同樣的,如果沒收到所有 client的回覆,就得凍結即時環境的運行直到收到所有回覆。在server 的即時環境的運行被凍結時,client 端會因為收不到server 傳來的下一輪命令串而同樣得凍結即時環境的運行。為了避免無止盡的等待,在等待時加入timeout機制來限制等待時間。

以上描述可以下圖表示,每一個參予者的每一次執行命令迴圈由三部份組成:
  1. 等待m 個frame。client 以m 個frame的緩衝時間接收命令串。
  2. 執行命令串。
  3. 等待n 個frame。server 以n 個frame的緩衝時間接收所有client 的回覆(reply)。
如果超過等待時間,沒取得該收到的資料,則會進入凍結環境的狀態(frozen game world)。如果再過一段時間還是沒收到資料(timeout),則進入處理斷線的程序。



上圖只是初步的構想,也許有其他更簡潔的傳輸協定方式來達成在即時性環境中的執行命令同步。
在更單純如回合制的環境下,server-client 的傳輸機制將會更簡單。這部份的討論留待將來再進行。



----

  1. 操作方式
  2. 程式架構
  3. 網路設定、接收與傳送
  4. Server - Client 間的傳輸協定
  5. 傳輸命令格式
  6. GUI
  7. 中文輸入與顯示
  8. 事件驅動

聊天室練習 - V. 傳輸命令格式

在聊天室這個練習中,網路接收與傳送是以單一物件為主,見"III、 網路設定、接收與傳送"。在這個架構下,每個命令以一個python內建型態--字典(dictionary )來包裝,內含三個欄位:
  1. "i":傳送命令者的索引代號
  2. "fn":命令名稱
  3. "param":命令參數

欄位"i"又分成兩種。
  • i=0:系統命令,處理特殊命令如參予者加入命令,參予者離開等事件。
  • i != 0:參予者輸入的命令。如聊天字串輸入,鍵盤按下,搖桿輸入等。
在 此練習中,參予者暱稱清單是由"i=1"開始算起,給第一個加入聊天室的server。"i"是交由server端依據封包由哪個client傳入來設定,只有server端擁有所有client的連線清單,隨者client加入或離開,此client的連線清單也跟著改變,因此client所表示的索 引代號"i"也會跟著改變。

欄位"fn"總是字串,定義在CommandInputLib.py中。欄位"param"隨著"fn"而有所不同格式。
此練習中使用下列"fn":
  • cmd_CLIENTINITIAL:client加入時的初始化處理。在此server將傳遞聊天室內的參予者暱稱清單,但只回覆給傳送此命令的client,而不是給所有的參予者。
  • cmd_USERJOIN:其他client加入時的處理。將加入者的暱稱加入參予者暱稱清單。
  • cmd_USERLEAV:其他client離開時的處理。將離開者的暱稱從參予者暱稱清單移除。
  • cmd_TEXT:參予者的對話字串處理。將此字串與發話者暱稱連結("發話者暱稱:字串"),加入文字顯示區。
由 於server端的client的連線清單,會隨著client加入或離開改變,server必須以命令cmd_USERJOIN、 cmd_USERLEAV來通知client,好讓client更新目前參予者暱稱清單,才能在處理命令cmd_TEXT時,由"i"取得正確的發話者暱 稱。參予者暱稱清單放於ChatRoomFrame.userList(ChatRoomFrame.py,第31行)。

傳輸命令格式的設計目的是希望藉由"i"來分辨參與者,將"fn"與"param"傳遞給對應的參予者處理。如此參予者的行為可交由不同人獨立撰寫,自行定義私人的傳輸命令,僅以一個共同介面如
CmdExec(fn,param)
來接收命令。



----

  1. 操作方式
  2. 程式架構
  3. 網路設定、接收與傳送
  4. Server - Client 間的傳輸協定
  5. 傳輸命令格式
  6. GUI
  7. 中文輸入與顯示
  8. 事件驅動

聊天室練習 - I. 操作方式

開啟:
執行 python -E ChatRoom.py


操作:
  1. login:輸入Nickname,選擇"Create a Chat Room"(建立聊天室)或是"Join to"(加入)。加入的一方(client)輸入建立的一方(server)的ip 在其後。按下"Login"按鈕以建立連線,成功建立連線後進入步驟2。
  2. chat room: 下方輸入文字(目前只處理英數)後按enter送出文字。"Host IP"按鈕可以顯示server 的ip 位址。




----

  1. 操作方式
  2. 程式架構
  3. 網路設定、接收與傳送
  4. Server - Client 間的傳輸協定
  5. 傳輸命令格式
  6. GUI
  7. 中文輸入與顯示
  8. 事件驅動

聊天室練習 - II. 程式架構

起初的構想是以一個對話框來取得暱稱及連線方式。如果用wxPython來做,應該會是個blockiing 的對話框。但在panda3D並沒有找到現有的對話框函數,因此將操作流程設計成有限狀態機,其行為如下圖描述。
Panda3D的FSM介紹參考這裡


當程式啟動時,進入Login狀態(ChatRoomMain.py,第43行),顯示Login界面(ChatRoomMain.py,第47行)來輸入 暱稱與連線方式,選擇成為server端(建立聊天室)或是client端(加入),成功建立連線後進入chatRoom狀態 (ChatRoomMain.py,第157行),顯示聊天室介面(ChatRoomMain.py,第55行),連線失敗則仍停留在Login狀態。
於ChatRoom狀態下,當client端失去與server的連線時,回到Login狀態(ChatRoomMain.py,第96行),若server端失去與client的連線則仍停留在ChatRoom狀態。
server端與client端以各別的方式,在ChatRoom狀態下,處理聊天室的文字輸入與顯示流程。server端與client端間的傳輸協定的目標為執行命令的同步--server端與所有client端都顯示相同順序的文字。
  • server端收集所有的文字輸入,包含server端與所有client端的文字輸入,再將收集的文字輸入串傳送給所有client,server再將這些收集的文字輸入串顯示於自身gui上。
  • client端送出自身的文字輸入給server端,如果收到server傳送的文字輸入串則顯示於自身gui。

詳細的server端與client端間的傳輸協定以及gui設計將稍後說明。



----

  1. 操作方式
  2. 程式架構
  3. 網路設定、接收與傳送
  4. Server - Client 間的傳輸協定
  5. 傳輸命令格式
  6. GUI
  7. 中文輸入與顯示
  8. 事件驅動

聊天室練習

以一個簡單的聊天室來練習panda3D中的2d gui 與 server - client 架構的網路設定。程式碼可以從這裡下載。

分成八個部份討論:

  1. 操作方式
  2. 程式架構
  3. 網路設定、接收與傳送
  4. Server - Client 間的傳輸協定
  5. 傳輸命令格式
  6. GUI
  7. 中文輸入與顯示
  8. 事件驅動


資料夾中包含以下檔案:

ChatRoom.py
執行點

ChatRoomMain.py
chat room主類別。於第II、IV、VIII段討論。

LoginFrame.py
login畫面組成。於第VI段討論。

ChatRoomFrame.py
chat room畫面組成。於第VI段討論。

ScrolledTextFrame.py
可捲動文字顯示框, 使用於chat room畫面組成中。於第VI段討論。

NetworkLib.py
網路操作模組。於第III段討論。

CommandInputLib.py
命令傳輸定義與函數。於第V、VIII段討論。

UnicodeDefine.py
unicode設定。於第VII段討論。

2010年2月3日 星期三

多個event handler接收同一個event的執行結果

panda3d的event可以參考
http://www.panda3d.org/wiki/index.php/Event_Handlers
以下例子是測試多個event handler接收同一個event的執行結果。

from direct.showbase import DirectObject
import direct.directbase.DirectStart

class EventHandler(DirectObject.DirectObject):
    def __init__(self,id):
        self.accept('test',self.eventHandle)
        self.id = id
    def eventHandle(self):
        print "eventHandle in EventHandler(%d)"%(self.id)

objectList = [EventHandler(i) for i in xrange(10)]
messenger.send('test')
run()

執行結果如下:

eventHandle in EventHandler(9)
eventHandle in EventHandler(8)
eventHandle in EventHandler(2)
eventHandle in EventHandler(7)
eventHandle in EventHandler(1)
eventHandle in EventHandler(6)
eventHandle in EventHandler(0)
eventHandle in EventHandler(5)
eventHandle in EventHandler(4)
eventHandle in EventHandler(3)

先產生10個不同的EventHandler物件,它們接收同一個event 'test'。
以messenger.send('test')發出event 'test'之後,這10個EventHandler物件都收到這個event,並執行各自的call back function。
從執行結果可以看到這10個EventHandler物件並不是依照被產生的順序接收到event,但每次執行的順序相同。

用event的方式可以分離產生event的物件,與執行此event的call back function物件。產生event的物件不必知道call back function函數名稱(參照),執行call back function的物件可以在執行期動態的指定(accept)或取消(ignore)對應event name的call back function,而不用改變產生event的物件。
缺點也在於它的優點,event機制如同全域變數,當兩個產生event的物件誤用了相同的event name時,會呼叫到其他接收相同event name的call back function,造成意外的錯誤。另外一個缺點在於,無法輕易的從產生event的地方得知此event的call back function為何,它可能藏在任何物件中,或是不存在。

2010年1月1日 星期五

利用python的執行期建立函數功能簡化Keyboard event宣告

Panda3D的Keyboard event可以從下列網址參考
主要是用self.accept( event_name, function ) 將event與function連結起來。
以下是一個例子:
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject

class keyHandler(DirectObject):
    def __init__(self):
        self.CallBackFuncRefMap = {}
        self.accept('a', self.KeyHandle_a)

    def AddKeyHandle(self, keyString, funcRef):
        self.CallBackFuncRefMap[keyString] = funcRef

    def KeyHandle_a(self):
        self.CallBackFuncRefMap['a']()


# testing
def callBack_a():
    print "exec: callBack_a"

test_keyHandler = keyHandler()
# bind callBack_a with keyboard event-'a'
test_keyHandler.AddKeyHandle('a', callBack_a)

run()
self.accept('a', self.KeyHandle_a)將鍵盤事件'a'與KeyHandle_a連結。
self.CallBackFuncRefMap是一個call back函數(call back函數不一定會宣告在keyHandler類別內)的hash table,以鍵盤事件名稱為key。經由AddKeyHandle函數新增此hash table的內容,如此便可在self.KeyHandle_a內呼叫。
新增鍵盤事件時,除了增加self.accept( event_name, function )之外,對應於event_name的function也得新增,如同上面的self.accept('a', self.KeyHandle_a)與KeyHandle_a,然而對應的function的內容有很大部分是相同的。因此可利用python於runtime建立函數的功能,將keyHandler類別改寫如下:
class keyHandler(DirectObject):
    def __init__(self):
        self.CallBackFuncRefMap = {}

    def AddKeyHandle(self, keyString, funcRef):
        self.CallBackFuncRefMap[keyString] = funcRef
        def callBackFunc():
            self.CallBackFuncRefMap[keyString]()
        self.accept(keyString, callBackFunc)
於AddKeyHandle函數內建立callBackFunc,並執行self.accept(keyString, callBackFunc)完成鍵盤事件與call back函數的連結。如此新增鍵盤事件只要呼叫AddKeyHandle,而不用在keyHandler類別內新增對程式碼了。