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)

沒有留言: