2009年12月15日 星期二

從hexabots 討論有限狀態機

hexabots是一個回合制戰略遊戲範例,可以從下列網址取得:
http://code.google.com/p/hexabots/
分成遊戲主程式部份(play.py),與遊戲關卡製作部分(edit.py)。這裡僅分析遊戲主程式部分的有限狀態機,關於panda3D的有限狀態機介紹可以參考
http://www.panda3d.org/wiki/index.php/Finite_State_Machines

PlayState是有限狀態機類別,其中含有五個狀態:
  1. AwaitLoad:等待地圖載入階段
  2. Charge:遊戲流程判斷
  3. Team1:Team1行動階段
  4. Team2:Team2行動階段
  5. PlayAnim:動畫執行階段
從play.py分析PlayState的狀態轉移如下圖所示:

程式啟動後,由 app.state.request('AwaitLoad') 將 FSM 從 'Off' 狀態切至 'AwaitLoad' ,並註冊 Load 函數等待地圖載入。地圖載入後經由 Load 函數中的 state.request('Charge') 將狀態切至 'charge' 。

在 charge 函數呼叫 app.state.request('Team1', character) 或 app.state.request('Team2', character) ,由於 'charge' 狀態定義了 'filterCharge' 函數,使得在 'charge' 狀態下執行 request 函數都會先經過 filterCharge 函數來決定下一個轉移的狀態。 filterCharge 經由判斷是否有動畫需要執行來決定要轉移至 'PlayAnim' 狀態執行動畫,或是轉移至 'Team1' 或 'Team2' 。 'Team1' , 'Team2' 或 'PlayAnim' 狀態執行結束後會經由 request 函數轉移至 charge 狀態。



分析:
  1. 執行狀態轉移的'request'函數並不完全在某個函數或類別中, 而是散佈在許多函數裡。這樣做不太容易掌握狀態轉移的過程,應該把所有 request 函數集中在一起。
  2. 'Charge'與'Team1','Team2'及'PlayAnim'可視為'遊戲中'狀態。'AwaitLoad'可視為進入遊戲前的'選單'狀態。 可以將原先一個有限狀態機拆成兩個,一個負責'主要流程控制',另一個負責'遊戲內的狀態控制'。
主要流程控制舉例如下:


主要流程控制不外乎是執行主選單的動作,開始新遊戲或是載入遊戲或設定遊戲環境及離開。遊戲過程中可呼叫另一個選單執行儲存遊戲,載入遊戲,回到遊戲,設定遊戲環境,回到主選單,或是離開等等。每個遊戲的主要流程控制部份應該不會差太多。

遊戲內的狀態控制舉例如下:



    當主要流程控制以 NewGame 或 LoadGame 進入遊戲時,遊戲內的流程控制會先切至 'Launch' 狀態來載入遊戲內容。接著開始遊戲執行迴圈:先等待遊戲命令輸入,取得輸入後執行遊戲命令,動畫執行也在此階段依序執行。至此一個迴圈結束,如果有表單命令可在此執行,儲存遊戲或是回到主選單等等。

    2009年8月9日 星期日

    Some simple dialogs of wxPython

      以下四個是我常用的wxPython對話框(或許之後還會陸續增加),包成函數形式省卻了部分填參數與開啟對話框,消除(Destroy)對話框的動作。


    import wx

    def SingleChoiceGetDialog(wnd, ItemNameList, dlgTitle = "", hintString = ""):
    """\
    SingleChoiceGetDialog(wnd, ItemNameList, dlgTitle = "", hintString = "")
    Get selected index in given items.

    Parameters:
    wnd ( type = parent window )
    ItemNameList ( type = list of string )
    dlgTitle ( type = string )
    hintString ( type = string )
    Returns:
    selectedIndex( type = int or None )"""
    dlg = wx.SingleChoiceDialog(
    wnd, hintString, dlgTitle,
    ItemNameList,
    wx.CHOICEDLG_STYLE
    )

    if dlg.ShowModal() == wx.ID_OK:
    selectedIndex = dlg.GetSelection()
    else: selectedIndex = None
    dlg.Destroy()
    return selectedIndex

    def FilePathGet(wnd, op, wildcard, dlgTitle):
    wildcard = "|".join(["""%s file (*.%s)|*.%s"""%(fileType, subName, subName)
    for (fileType, subName) in wildcard])

    style = (wx.SAVE | wx.CHANGE_DIR) if op == "save" else (wx.OPEN | wx.CHANGE_DIR)
    FileDlg = wx.FileDialog( wnd, dlgTitle,
    defaultDir="",
    defaultFile="",
    wildcard = wildcard,
    style=style
    )

    if( FileDlg.ShowModal() == wx.ID_OK ):
    filePath = FileDlg.GetPath()
    else: filePath = None
    FileDlg.Destroy()
    return filePath

    def FileOpenPathGetDialog(wnd, wildcard = [("All","*")], dlgTitle = "File Open"):
    """\
    FileOpenPathGetDialog(wnd, wildcard = [("All","*")], dlgTitle = "File Open")
    Get selected file path on action of "FileOpen".

    Parameters:
    wnd ( type = parent window )
    wildcard ( type = list of tuple )
    dlgTitle ( type = string )
    Returns:
    filePath ( type = string or None)"""
    return FilePathGet(wnd, "open", wildcard, dlgTitle)

    def FileSavePathGetDialog(wnd, wildcard = [("All","*")], dlgTitle = "File Save"):
    """\
    FileSavePathGetDialog(wnd, wildcard = [("All","*")], dlgTitle = "File Save")
    Get selected file path on action of "FileSave".

    Parameters:
    wnd ( type = parent window )
    wildcard ( type = list of tuple )
    dlgTitle ( type = string )
    Returns:
    filePath ( type = string or None )"""
    return FilePathGet(wnd, "save", wildcard, dlgTitle)

    def ErrorMessageDialog(wnd, msg):
    """\
    ErrorMessageDialog(wnd, msg)
    Popup an error message.

    Parameters:
    wnd ( type = parent window )
    msg ( type = string )"""
    dlg = wx.MessageDialog(wnd, msg, "Error", wx.OK | wx.ICON_ERROR )
    dlg.ShowModal()
    dlg.Destroy()


      SingleChoiceGetDialog:從ItemNameList中單選。

      FileOpenPathGetDialog, FileSavePathGetDialog:取得"開檔"/"存檔"的檔案路徑。
        想顯示副檔名為"py"的檔案("*.py"),就在加入wildcard中加入tuple ("py","py"),前一個"py"是提示字串,後一個"py"是用來過濾副檔名的字串。

      ErrorMessageDialog:彈出錯誤訊息。

      對於GUI,我傾向花越少時間處理越好;漂亮,獨特的外觀是不太在乎的,只要個普通的window類型的表現,能讓使用者了解的動作提示,例如一看就知道這是要開啟檔案,就足夠了。所以我用了許多預設參數簡化函數呼叫。例如在frame中開啟檔案--

    filePath = FileOpenPathGetDialog(self) # "self" is frame object in this case
    if filePath: data = open(filePath,"w")

    2009年8月8日 星期六

    cPickle - 儲存python物件至檔案 / 從檔案中讀取python物件

    cPickle可以在檔案中保存python物件,讀取時不用再另外寫parser重組資訊。
    import cPickle
    
    def ObjectSaveToFile(filePath, obj):
        """\
    ObjectSaveToFile(filePath, obj)
        Save a python object into file.
    
        Parameters:
            filePath     ( type = string)
            obj          ( type = any python object like dict )"""
        fileGen = open(filePath, 'w')
        cPickle.dump(obj, fileGen, True)
        fileGen.close()
    
    def ObjectLoadFromFile(filePath):
        """\
    ObjectSaveToFile(filePath, obj)
        Load a python object from file.
        Parameters:
            filePath     ( type = string)
        Returns:
            obj          ( type = any python object like list, dict )"""
        fileRead = open(filePath, 'r')
        obj = cPickle.load(fileRead)
        fileRead.close()
        return obj
    


    ObjectSaveToFile函數設計成在檔案中保存單一物件。雖然cPickle支援儲存多個物件至檔案中,但讀取時也得照順序讀。這會造成程式維護的相容性問題:當改變讀取或存入物件順序後,舊的檔案便無法再讀取,或是得特別保留讀取舊檔案格式的程式碼。

    因此我會以字典(dict)物件將多個物件包成單一物件,以key來讀取各別物件。objDict.get(key, defaultObj)可用來保持與舊檔案格式的相容,當沒有找到某個key時,將對應物件設定成預設值。

    2009年8月7日 星期五

    A test for syntaxhighlighter

    before...

    for i in range(10):
    print "hello"

    =====

    after...

    for i in range(10):
    print "hello"