![]() |
||||
|
The Solitaire Pack engine (implemented in the CEngine class) keeps track of two states: one for the engine in general and one for the state of the mouse. Engine State The engine state dictates what actions can be performed at the current time. The following are the engine state values:
The final three entries are not separate states, but are flags that are or'd with the current state. A macro, ENGSTATE, returns the state stripped of any of these flags that may be present. The engine state is not managed by a centralized state machine control; however, the following diagram shows what valid transitions exist between states (excluding the flag states):
Keeping track of engine state is important to controlling what actions can take place at what time. For example, the only time that game objects can create piles (via ISolpackApp::CreatePile) is from their Initialize function. Thus, the CEngine implementation of CreatePile begins with the following statement:
if(ENGSTATE != ENG_STARTGAME) return E_ACCESSDENIED; Thus, any attempt that a game object makes to create a pile during, say, a DropFragment event, will fail. Most of the engine states correspond to a ISolpackGame method that is being processed. Thus, if the game's implementation of one of the methods makes a call back to the engine object, the engine has an easy way to check to see if the call can be processed. The ENG_UNDO flag is important in controlling when user actions are recorded on the undo stack and when they are not. When the engine has to carry out a user action (such as turning over a card), it puts all data for the user action into a CAction object. The engine then carries out the action by calling whatever code actually performs the action. Then, if ENG_UNDO is not present in the engine state, the undo manager records the action in the undo stack. The presence of ENG_UNDO would signify that the processing of the action is in response to the Redo command rather than the initial time the user performs the action; thus, the action would not need to be recorded. Overall, keeping track of engine state is very useful. It provides a quick way to, using a single value, check what state the engine is in and what can be performed. The old implementation of Solitaire Pack used a number of boolean variables and other means to keep track of individual things to check for, such as has the user interface been loaded fully or whether the engine is currently calling the game's ISolpackGame::Initialize method. Checking for a valid state before processing an action would require checking a number of different variables; as such, the previous version did very little checking for valid processing. Using engine state is a significant improvement in this version of Solitaire Pack. Mouse State Much of the Solitaire Pack engine's logic revolves around processing user mouse actions. For instance, the game's ISolpackGame::PileClicked method is called when the engine receives a left mouse button down event on a pile or card, followed by a left mouse button up event. Other mouse events include moving the mouse and clicking with different buttons. Before doing any programming, and without looking at any of the mouse event handling code from my previous implementation, I analyzed which mouse events would cause which game actions. Using Rational Rose, I developed a state chart diagram detailing the various mouse states and how mouse events influence what action the user is performing.
Before continuing, I will give a brief explanation of Solitaire Pack's event handling mechanism. The entire Solitaire Pack interface is implemented with Internet Explorer components (the MSHTML renderer); card and pile objects are displayed onscreen as HTML Span elements. MSHTML allows programs to "handle" events, such as the mouse button being pressed or released on an element. Because event handlers are used so commonly in many of my programs, I wrote a generic CEventHandler class that encapsulates generic event handler capabilities. The CCard and CPile classes, which create and manage the MSHTML element objects, create and bind event handlers to the following events:
So, for instance, when the mouse is pressed down over a card element, the card's onmousedown event handler is notified. In all cases, the CCard and CPile classes forward all mouse events to the engine (CEngine), via the following CEngine functions:
The MouseDown and MouseUp functions are also passed information about what button was pressed or released. The particular event, combined with button information, make up the "transition" between mouse states; these functions use the current state and this "transition" to determine the new mouse state, and what action, if any, to perform. For the most part, each of the functions use a switch statement to look at the current state and then assign a new state, and optionally perform tasks. The following code fragments, although simplified slightly, convey the ease with which mouse events are processed. //---------------------------------------------------------------------------- // // IEngineObjSite::MouseDown method // Called when a mouse button has been pressed on a pile or card. // // Parameters: // pObj: The pile or card which was clicked. // lCurButton: Value specifiying which mouse buttons are down. // // Returns: TRUE if the object should capture focus, else FALSE. // //---------------------------------------------------------------------------- BOOL CEngine::MouseDown(CEngineObj *pObj, long lCurButton) { // Notes: // 1. m_lButton is a member variable which tracks which buttons // are down between events. // 2. Values: Buttons are a combination of the following values: // 0: no button // 1: left button // 2: right button // 3: middle button // 3. SETMS sets the mouse state; SetState sets the engine state. // Update engine state SetState(m_dwState | ENG_MOUSEDOWN); // Figure out which button was just pressed down long lNewButton = lCurButton - m_lButton; m_lButton = lCurButton; // If the engine is not idle, don't do anything if(ENGSTATE != ENG_IDLE) { SETMS(MS_WAITALL); return TRUE; } // First: handle if it is a pile // (code omitted) // Otherwise, it is a card. // Look at current state, and decide on new state. switch(m_dwMouseState) { case MS_IDLE: m_pCardSrc = (CCard*)pObj; m_pPileSrc = (CPile*)(m_pCardSrc->GetPile()); // left clicked? if(lNewButton == 1) { // Set new state SETMS(MS_CARD_LBD); // Track what coordinate the mouse is at CHTMLEventObjPtr spEvent = m_pHH->GetEvent(); CHECKHRCOM(spEvent->get_clientX(&m_ptDrag.x)); CHECKHRCOM(spEvent->get_clientY(&m_ptDrag.y)); } else // right clicked? if(lNewButton == 2) { // Set new state SETMS(MS_CARD_RBD); } else // anything else--wait for user to release all buttons { SETMS(MS_WAITALL); } return TRUE; break; // Clicking while in any of these states makes us wait for all // buttons to be released before continuing case MS_PILE_LBD: case MS_CARD_LBD: case MS_CARD_LBDNODRAG: case MS_CARD_RBD: SETMS(MS_WAITALL); break; // All other states not listed (MS_WAITALL, MS_DRAGGING) have // transitions to themselves after a button click. Hence they are // not listed. } return FALSE; } //---------------------------------------------------------------------------- // // IEngineObjSite::MouseUp method // Called when a mouse button has been released. // // Parameters: // bOverEl: Whether the mouse is being released over the same element // as that which has the focus. // lButton: Value specifiying which mouse button was released. // // Returns: TRUE if the object should release mouse focus, else // FALSE. // //---------------------------------------------------------------------------- BOOL CEngine::MouseUp(BOOL bOverEl, long lButton) { // Update stored button value m_lButton &= ~lButton; // If we were animating, a click cancels the animation (lets the user bypass // lengthy animation) if(m_dwState & ENG_ANIM) m_pFragment->SetCancelAnim(TRUE); // was left button released? if(lButton == 1) { switch(m_dwMouseState) { // In each of these first three cases, the net effect is that the pile/card was clicked. case MS_PILE_LBD: case MS_CARD_LBD: case MS_CARD_LBDNODRAG: if(bOverEl) { // Update the mouse state and engine state, and handle the event SETMS(MS_PILE_CLICKED); SetState((m_dwState & ENG_FLAGS) | ENG_PILECLICKED); HandlePileClicked((WPARAM)m_pPileSrc, (LPARAM)m_pCardSrc); } // After processing event, set back to idle. SETMS(MS_IDLE); break; // We were dragging, so try to find a drop target case MS_DRAGGING: // Update mouse state and engine state SETMS(MS_TRYDROP); SetState((m_dwState & ENG_FLAGS) | ENG_DROPFRAG); HandleTryDrop((WPARAM)m_pPileSrc, (LPARAM)m_pCardSrc); // wait for all other mouse buttons, if any, to be released before continuing SETMS(MS_WAITALL); break; } } else // process right button release (code omitted) // Before returning from the function: if(m_dwMouseState == MS_WAITALL) { // Check to see if the Waitall has been completed // (That is, if all other buttons are released) if(m_lButton == 0) // If so, set mouse state back to idle. SETMS(MS_IDLE); else return FALSE; } return TRUE; } The first time I sat down and write the basics of these functions, based on the mouse state diagram, things performed reasonably well. There were a number of minor issues which complicate things, but the main concept of simply using state and transition to determine new state has remained. Thus, by thinking of the mouse logic in terms of a state machine, I was able to easily implement what I'd thought of as one of the more challenging parts of the program. The reason I thought this would be so difficult was because the way that I implemented mouse processing in the old version of Solitaire Pack was very complicated and ineffective. For one thing, instead of having three event functions in the engine, I had one function which had to determine first what event had occurred. Just for the sake of comparison, the following function from the old version is that which is roughly equivalent to the functions above. Don't even try to understand the code; I didn't even understand it after not looking at it for nearly a year. // Function: CardEventHandler // // Purpose: Handles MSHTML events from the pile's SPAN element and from child card // elements. // // Parameters: // dispidEvent: ID of the event that took place. // pPile: Pile which contains the card. // pCard: Card which is sending the event. If NULL, the event is from the pile. // // Returns: None. // void CAppImpl::CardEventHandler(DISPID dispidEvent, CSolpackPile *pPile, CSolpackCard *pCard) { if(dispidEvent == DISPID_HTMLELEMENTEVENTS2_ONMOUSEUP) { if(m_bAnimWon) { CancelWon(); return; } if(m_bBusy) { m_bCancelAnims = TRUE; return; } } if(m_bBusy) return; if(!pPile) { //OutputDebugString(TEXT("CardEventHandler: No Pile\n")); return; } IHTMLEventObj *pEventObj = NULL; if(m_pHTMLWindow2) m_pHTMLWindow2->get_event(&pEventObj); if(!pEventObj) return; HRESULT hrPileClicked = E_FAIL; switch(dispidEvent) { case DISPID_HTMLELEMENTEVENTS2_ONMOUSEDOWN: if(!m_pDragSource && (m_lMouseState == CARD_MS_NONE)) { long lButton = 0; pEventObj->get_button(&lButton); m_lMouseState = lButton; //if(lButton & CARD_MS_LEFT) //{ if(pCard) // Clicked on a card { m_pCardSource = pCard; m_pCardSource->AddRef(); pEventObj->get_offsetX(&m_lOffsetX); pEventObj->get_offsetY(&m_lOffsetY); // m_lOffsetX += pCard->m_lX; // m_lOffsetY += pCard->m_lY; } else // Clicked on an empty pile { m_pDragSource2 = pPile; m_pDragSource2->AddRef(); m_bCheckDistrib = TRUE; //m_lMouseState |= CARD_NOCARD; } //} } break; case DISPID_HTMLELEMENTEVENTS2_ONMOUSEMOVE: //OutputDebugString(TEXT("CardEventHandler: ONMOUSEMOVE")); //if(!(m_lMouseState & CARD_MS_LEFT)) // break; if(!m_pDragSource && (m_lMouseState == CARD_MS_LEFT)) { //OutputDebugString(TEXT(" > CARD_MS_LEFT")); if(pCard && (pCard == m_pCardSource)) { // We just started a drag if(pCard->m_bCanDrag) { //OutputDebugString(TEXT(" > Same Card")); ISolpackCard *pSPCard = NULL; if(SUCCEEDED(pCard->QueryInterface(IID_ISolpackCard, (LPVOID*)&pSPCard))) { ISolpackPile *pSPPile = NULL; if(SUCCEEDED(pPile->QueryInterface(IID_ISolpackPile, (LPVOID*)&pSPPile))) { ISolpackPile *pSPFrag = NULL; if(SUCCEEDED(m_pFragment->QueryInterface(IID_ISolpackPile, (LPVOID*)&pSPFrag))) { long lType = 0; pPile->get_Type(&lType); m_pFragment->SetType(lType); long lIndexOfCard = -1; pPile->IndexOfCard(pSPCard, &lIndexOfCard); if(lIndexOfCard != -1) { VARIANT_BOOL bOnlyCard = VARIANT_FALSE; if(SUCCEEDED(m_pCurrentGame->SetupFragment(pSPPile, pSPCard, lIndexOfCard, pSPFrag, &bOnlyCard))) { m_pFragment->m_pFragSrc = pPile; long lNewX = 0; long lNewY = 0; pEventObj->get_x(&lNewX); pEventObj->get_y(&lNewY); //CSolpackCard *pFirstCard = m_pFragment->GetFirstCard(); //if(pFirstCard) { m_lOffsetX += pCard->m_lX; m_lOffsetY += pCard->m_lY; //pFirstCard = NULL; } m_pFragment->put_X(lNewX - m_lOffsetX); m_pFragment->put_Y(lNewY - m_lOffsetY);//(m_lY + lNewY); m_pFragment->UpdateSpanPos(0, 0, USP_X | USP_Y); if(bOnlyCard == VARIANT_TRUE) { IDispatch *pUnkCard = NULL; if(SUCCEEDED(pSPCard->QueryInterface(IID_IDispatch, (LPVOID*)&pUnkCard))) { m_pFragment->AddToFragment(pUnkCard); SAFERELEASE(pUnkCard); } } long lNumCardsAfter = 0; m_pFragment->get_NumCards(&lNumCardsAfter); if(lNumCardsAfter > 0) { SAFERELEASE(m_pDragSource); m_pDragSource = pPile; m_pDragSource->AddRef(); //m_lMouseState |= CARD_MS_DRAGGING; IHTMLElement2 *pEl2 = NULL; if(SUCCEEDED(m_pDragSource->m_pElPileSpan->QueryInterface (IID_IHTMLElement2, (LPVOID*)&pEl2))) { pEl2->setCapture(VARIANT_TRUE); SAFERELEASE(pEl2); m_lNeedToRelease = NTR_PILE; //m_bDragging = TRUE; } } } } } } SAFERELEASE(pSPPile); } SAFERELEASE(pSPCard); } else { // Can't drag it, but keep track anyways //SAFERELEASE(m_pDragSource); //m_pDragSource = pPile; //m_pDragSource->AddRef(); //m_lMouseState |= CARD_MS_DRAGGING; //OutputDebugString(TEXT("Left Drag\n")); m_pDragSource2 = pPile; m_pDragSource2->AddRef(); m_bCheckDistrib = TRUE; /* IHTMLElement2 *pEl2 = NULL; if(SUCCEEDED(pPile->m_pElPileSpan->QueryInterface (IID_IHTMLElement2, (LPVOID*)&pEl2))) { pEl2->setCapture(VARIANT_TRUE); SAFERELEASE(pEl2); m_lNeedToRelease = NTR_PILE; }*/ } } else { m_pDragSource2 = pPile; m_pDragSource2->AddRef(); m_bCheckDistrib = TRUE; //OutputDebugString(TEXT("Cards don't match or no card\n")); } } else if(m_pDragSource) { //OutputDebugString(TEXT(" > has drag source")); long lNewX = 0; long lNewY = 0; pEventObj->get_x(&lNewX); pEventObj->get_y(&lNewY); m_pFragment->put_X(lNewX - m_lOffsetX); m_pFragment->put_Y(lNewY - m_lOffsetY);//(m_lY + lNewY); m_pFragment->UpdateSpanPos(0, 0, USP_X | USP_Y); } else //if(!m_bDragging)// Must be a different button down { // OutputDebugString(TEXT(" > some other button\n")); long lButton = 0; pEventObj->get_button(&lButton); if((lButton == CARD_MS_RIGHT) && (!m_lNeedToRelease)) { m_bCheckDistrib = TRUE; //OutputDebugString(TEXT(" > button not none")); //IHTMLElement2 *pEl2 = NULL; /*HRESULT hr; if(pCard) { hr = pCard->m_pElCard->QueryInterface(IID_IHTMLElement2, (LPVOID*)&pEl2); if(SUCCEEDED(hr)) m_lNeedToRelease = NTR_CARD; } else { hr = pPile->m_pElPileSpan->QueryInterface(IID_IHTMLElement2, (LPVOID*)&pEl2); if(SUCCEEDED(hr)) m_lNeedToRelease = NTR_PILE; }*/ /*if(SUCCEEDED(pPile->m_pElPileSpan->QueryInterface (IID_IHTMLElement2, (LPVOID*)&pEl2))) { pEl2->setCapture(VARIANT_TRUE); SAFERELEASE(pEl2); m_lNeedToRelease = NTR_PILE; }*/ } } //OutputDebugString(TEXT("\n")); break; case DISPID_HTMLELEMENTEVENTS2_ONMOUSEUP: { if(pCard) { pEventObj->put_cancelBubble(VARIANT_TRUE); } CSolpackCard *pOldCardSource = m_pCardSource; m_pCardSource = NULL; long lButton = 0; pEventObj->get_button(&lButton); if(m_lNeedToRelease) { IHTMLElement2 *pEl2 = NULL; /*HRESULT hr; if(m_lNeedToRelease == NTR_CARD) { if(pCard) hr = pCard->m_pElCard->QueryInterface(IID_IHTMLElement2, (LPVOID*)&pEl2); } else { hr = pPile->m_pElPileSpan->QueryInterface(IID_IHTMLElement2, (LPVOID*)&pEl2); } if(SUCCEEDED(hr))*/ if(SUCCEEDED(pPile->m_pElPileSpan->QueryInterface (IID_IHTMLElement2, (LPVOID*)&pEl2))) { pEl2->releaseCapture(); SAFERELEASE(pEl2); } m_lNeedToRelease = NTR_NONE; } if(m_pDragSource /*&& m_bDragging*/) { if((lButton & CARD_MS_LEFT) && m_pDragSource) // No left anymore { m_lMouseState = CARD_MS_NONE; //m_lMouseState &= ~CARD_MS_DRAGGING; long lX = 0; long lY = 0; pEventObj->get_x(&lX); pEventObj->get_y(&lY); HandleDragEnd(m_pFragment, lX, lY); SAFERELEASE(m_pDragSource); } } else // if((pCard && (pCard == pOldCardSource)) || (m_lMouseState & CARD_NOCARD)) //{ if((m_lMouseState & CARD_MS_LEFT) && (lButton & CARD_MS_LEFT)) // Left was clicked once { //m_lMouseState = CARD_MS_NONE; ISolpackPile *pSPThisPile = NULL; if(SUCCEEDED(pPile->QueryInterface(IID_ISolpackPile, (LPVOID*)&pSPThisPile))) { if(pCard) { if(m_pCurrentGame/*&& (g_cAnims == 0)*/) { BOOL bValid = FALSE; if(pCard == pOldCardSource) bValid = TRUE; else if(m_bCheckDistrib) { ISolpackPile *pCardsPile = NULL; if(pOldCardSource && SUCCEEDED(pOldCardSource->get_Pile(&pCardsPile))) { if(pCardsPile == pSPThisPile) bValid = TRUE; SAFERELEASE(pCardsPile); } } if(bValid) hrPileClicked = m_pCurrentGame->PileClicked(pSPThisPile); } } else { if(m_bCheckDistrib && m_pCurrentGame) { if(m_pDragSource2 && (m_pDragSource2 == pPile)) hrPileClicked = m_pCurrentGame->PileClicked(pSPThisPile); } // OutputDebugString(TEXT("Dragged away\n")); } SAFERELEASE(pSPThisPile); } //bSetToNone = TRUE; } else if((m_lMouseState & CARD_MS_RIGHT) && (lButton & CARD_MS_RIGHT)) { // Right was clicked once if(pCard && (pCard == pOldCardSource)) { m_lMouseState = CARD_MS_NONE; if(pPile->AutoPlayCard(pCard) == S_OK) PushNewTurn(); //bSetToNone = TRUE; } //m_lMouseState = CARD_MS_NONE; } m_lMouseState = CARD_MS_NONE; m_bCheckDistrib = FALSE; SAFERELEASE(m_pDragSource2); SAFERELEASE(pOldCardSource); } break; } if(hrPileClicked == S_OK) { PushNewTurn(); } SAFERELEASE(pEventObj); } There are so many if statements, so many different things kept track of in different places, so many lines commented out. The code was confusing and messy. Using a state machine in the new version is not solely responsible for the cleaner code this time around; my own programming practices have improved dramatically since that time. Also, other parts of my architecture have improved greatly, allowing for simpler and cleaner code. Overall, using a state machine to process mouse events has made the development of what was potentially the most complicated logic in the program relatively simple. In future development projects, I will definitely be on the lookout for places in which using a state machine can simplify program design and implementation. |
April 24, 2002 | jmhoersc@mtu.edu |