読者です 読者をやめる 読者になる 読者になる

S.F. Page

Programming,Music,etc...

Box2D + WinRTでゲームを作る (14) – 状態管理をBoost.MSMで作る

Boost シューティングゲーム C++ Windows ゲーム Box2D + WinRTでゲームを作る

下のソースコードはXAML DirectX 3D shooting game sampleのApp.xaml.cppのUpdateメソッドを引用したものである。このコードはゲーム状態管理の実装でもある。

//-------------------------------------------------------------------------------------- 
 
void App::Update() 
{ 
    m_controller->Update(); 
 
    switch (m_updateState) 
    { 
    case UpdateEngineState::ResourcesLoaded: 
        switch (m_pressResult) 
        { 
        case PressResultState::LoadGame: 
            SetGameInfoOverlay(GameInfoOverlayState::GameStats); 
            break; 
 
        case PressResultState::PlayLevel: 
            SetGameInfoOverlay(GameInfoOverlayState::LevelStart); 
            break; 
 
        case PressResultState::ContinueLevel: 
            SetGameInfoOverlay(GameInfoOverlayState::Pause); 
            break; 
        } 
        m_updateState = UpdateEngineState::WaitingForPress; 
        SetAction(GameInfoOverlayCommand::TapToContinue); 
        m_controller->WaitForPress(); 
        ShowGameInfoOverlay(); 
        m_renderNeeded = true; 
        break; 
 
    case UpdateEngineState::WaitingForPress: 
        if (m_controller->IsPressComplete() || m_pressComplete) 
        { 
            m_pressComplete = false; 
 
            switch (m_pressResult) 
            { 
            case PressResultState::LoadGame: 
                m_updateState = UpdateEngineState::WaitingForResources; 
                m_pressResult = PressResultState::PlayLevel; 
                m_controller->Active(false); 
                m_game->LoadGame(); 
                SetAction(GameInfoOverlayCommand::PleaseWait); 
                SetGameInfoOverlay(GameInfoOverlayState::LevelStart); 
                ShowGameInfoOverlay(); 
                m_renderNeeded = true; 
 
                m_game->LoadLevelAsync().then([this]() 
                { 
                    m_game->FinalizeLoadLevel(); 
                    m_updateState = UpdateEngineState::ResourcesLoaded; 
                    m_renderNeeded = true; 
                }, task_continuation_context::use_current()); 
                break; 
 
            case PressResultState::PlayLevel: 
                m_updateState = UpdateEngineState::Dynamics; 
                HideGameInfoOverlay(); 
                m_controller->Active(true); 
                m_game->StartLevel(); 
                break; 
 
            case PressResultState::ContinueLevel: 
                m_updateState = UpdateEngineState::Dynamics; 
                HideGameInfoOverlay(); 
                m_controller->Active(true); 
                m_game->ContinueGame(); 
                break; 
            } 
        } 
        break; 
 
    case UpdateEngineState::Dynamics: 
        if (m_controller->IsPauseRequested() || m_pauseRequested) 
        { 
            m_pauseRequested = false; 
 
            m_game->PauseGame(); 
            SetGameInfoOverlay(GameInfoOverlayState::Pause); 
            SetAction(GameInfoOverlayCommand::TapToContinue); 
            m_updateState = UpdateEngineState::WaitingForPress; 
            m_pressResult = PressResultState::ContinueLevel; 
            ShowGameInfoOverlay(); 
            m_renderNeeded = true; 
        } 
        else 
        { 
            GameState runState = m_game->RunGame(); 
            switch (runState) 
            { 
            case GameState::TimeExpired: 
                SetAction(GameInfoOverlayCommand::TapToContinue); 
                SetGameInfoOverlay(GameInfoOverlayState::GameOverExpired); 
                ShowGameInfoOverlay(); 
                m_updateState = UpdateEngineState::WaitingForPress; 
                m_pressResult = PressResultState::LoadGame; 
                m_renderNeeded = true; 
                break; 
 
            case GameState::LevelComplete: 
                SetAction(GameInfoOverlayCommand::PleaseWait); 
                SetGameInfoOverlay(GameInfoOverlayState::LevelStart); 
                ShowGameInfoOverlay(); 
                m_updateState = UpdateEngineState::WaitingForResources; 
                m_pressResult = PressResultState::PlayLevel; 
                m_renderNeeded = true; 
 
                m_game->LoadLevelAsync().then([this]() 
                { 
                    m_game->FinalizeLoadLevel(); 
                    m_updateState = UpdateEngineState::ResourcesLoaded; 
                    m_renderNeeded = true; 
 
                }, task_continuation_context::use_current()); 
                break; 
 
            case GameState::GameComplete: 
                SetAction(GameInfoOverlayCommand::TapToContinue); 
                SetGameInfoOverlay(GameInfoOverlayState::GameOverCompleted); 
                ShowGameInfoOverlay(); 
                m_updateState  = UpdateEngineState::WaitingForPress; 
                m_pressResult = PressResultState::LoadGame; 
                m_renderNeeded = true; 
                break; 
            } 
        } 
 
        if (m_updateState == UpdateEngineState::WaitingForPress) 
        { 
            // Transitioning state, so enable waiting for the press event 
            m_controller->WaitForPress(); 
        } 
        if (m_updateState == UpdateEngineState::WaitingForResources) 
        { 
            // Transitioning state, so shut down the input controller until resources are loaded 
            m_controller->Active(false); 
        } 
        break; 
    } 
} 
 
//-------------------------------------------------------------------------------------- 
 
void App::OnWindowActivationChanged( 
    _In_ Platform::Object^ /* sender */, 
    _In_ Windows::UI::Core::WindowActivatedEventArgs^ args 
    ) 
{ 
    if (args->WindowActivationState == CoreWindowActivationState::Deactivated) 
    { 
        m_haveFocus = false; 
 
        switch (m_updateState) 
        { 
        case UpdateEngineState::Dynamics: 
            // From Dynamic mode, when coming out of Deactivated rather than going directly back into game play 
            // go to the paused state waiting for user input to continue 
            m_updateStateNext = UpdateEngineState::WaitingForPress; 
            m_pressResult = PressResultState::ContinueLevel; 
            SetGameInfoOverlay(GameInfoOverlayState::Pause); 
            ShowGameInfoOverlay(); 
            m_game->PauseGame(); 
            m_updateState = UpdateEngineState::Deactivated; 
            SetAction(GameInfoOverlayCommand::None); 
            m_renderNeeded = true; 
            break; 
 
        case UpdateEngineState::WaitingForResources: 
        case UpdateEngineState::WaitingForPress: 
            m_updateStateNext = m_updateState; 
            m_updateState = UpdateEngineState::Deactivated; 
            SetAction(GameInfoOverlayCommand::None); 
            ShowGameInfoOverlay(); 
            m_renderNeeded = true; 
            break; 
        } 
        // Don't have focus so shutdown input processing 
        m_controller->Active(false); 
    } 
    else if (args->WindowActivationState == CoreWindowActivationState::CodeActivated 
        || args->WindowActivationState == CoreWindowActivationState::PointerActivated) 
    { 
        m_haveFocus = true; 
 
        if (m_updateState == UpdateEngineState::Deactivated) 
        { 
            m_updateState = m_updateStateNext; 
 
            if (m_updateState == UpdateEngineState::WaitingForPress) 
            { 
                SetAction(GameInfoOverlayCommand::TapToContinue); 
                m_controller->WaitForPress(); 
            } 
            else if (m_updateStateNext == UpdateEngineState::WaitingForResources) 
            { 
                SetAction(GameInfoOverlayCommand::PleaseWait); 
            } 
 
            // App is now "active" so set up the event handler to do game processing and rendering 
            if (m_onRenderingEventToken.Value == 0) 
            { 
                m_onRenderingEventToken = CompositionTarget::Rendering::add(ref new EventHandler<Object^>(this, &App::OnRendering)); 
            } 
        } 
    } 
} 

このように普通に実装するとSwitch文のネスト、if文の連続となる。比較的簡単であるゲームの状態管理でさえこのようになる。面倒なのは状態が増えたときのメンテナンスである。状態が増えたことによって、各状態の内容を細かく見直さなくてはならなくなる。Boost.MSMにすると状態・アクション・遷移がテーブル化され、メンテナンスが比較的容易になる。また状態フローも非常に見やすくなる。と私は思っている。

この部分をBoost.MSMにどのように持っていくか今思案しているところである。状態マシンは別クラスとしてSimple3DGame.hの変数として持たせようかなと思っている。

ゲームエディット機能も作るつもりなので、管理する状態は増える。Boost.MSMで実装しきれるのか今一つ不安であるが進めることにする。