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