Tutorial #03: Card Game

Based on the previous tutorials, we are now going to explain, how a simple card game can be implemented with the techniques shown so far.

This tutorial especially explains how multiple instances of graphic objects can be elegantly used – in our case a whole card deck.

The fonts from the bitmap font tutorial were transferred to the directory "fonts" and the corresponding script was adjusted accordingly.

For our tutorial we have downloaded pre-built card graphics from https://code.google.com/p/vector-playing-cards and added them to the directory PNG-cards-1.3.

  • data/orig/fonts
  • data/orig/PNG-cards-1.3/clubs
  • data/orig/PNG-cards-1.3/diamonds
  • data/orig/PNG-cards-1.3/hearts
  • data/orig/PNG-cards-1.3/misc
  • data/orig/PNG-cards-1.3/spades

As a texture containing all cards would become too large, we decide to create an individual texture for every card suit. For this purpose, the cards are moved to same-named sub-directories in order to allow creating individual card textures with a configuration file.

  • scripts/atlas_config_cards.xml

This configuration file has the special feature of using an external attribute named cardType. External attributes are set in the script when calling the atlas_generator.

<?xml version="1.0"?>
<!-- Copyright 2013 Spraylight GmbH -->

<AtlasGenerator cardType="">
    
    <Input path="../data/orig/PNG-cards-1.3/{cardType}">
        
        <Matte color="0i, 0i, 0i"/>
        
        <Image scanAll="yes" sizeX="90" sizeY="130"/>
        
    </Input>
    
    <Output path="../data/packages/game.murlres">
        
        <Atlas sizeRaster="2"/>
        
        <Image name="gfx_{cardType}.png" margin="1"/>
        
        <PlaneGraphXML name="planes_{cardType}.xml"/>
        
    </Output>
    
</AtlasGenerator>

Here, it can be seen that the cardType attribute is used on several positions as substitute. Additionally, a pre-defined value can be set for external attributes in the root tag. <AtlasGenerator cardType=""> does not affect anything, but it is indicated for demonstration purposes.

As the cards have a rather high resolution, we scale them to a suitable size by using <Image scanAll="yes" sizeX="90" sizeY="130"/>. Here, it can be clearly seen that scaling with a large scaling factor results in a loss of quality. For our tutorial, this is tolerable, but it is recommended to download the vector data of the cards and to save them as PNG in the desired size.

The corresponding script calls the atlas_generator as follows:

%MURL_ATLAS_GENERATOR% -q --config atlas_config_cards.xml --attribute cardType="clubs"
%MURL_ATLAS_GENERATOR% -q --config atlas_config_cards.xml --attribute cardType="diamonds"
%MURL_ATLAS_GENERATOR% -q --config atlas_config_cards.xml --attribute cardType="hearts"
%MURL_ATLAS_GENERATOR% -q --config atlas_config_cards.xml --attribute cardType="spades"
%MURL_ATLAS_GENERATOR% -q --config atlas_config_cards.xml --attribute cardType="misc"

Consequently, a texture and a corresponding graph file are generated from the single-images in the five directories.

Scene Graph

For this tutorial, the scene graph is divided in several different files with the prefix "graph_".

<?xml version="1.0" ?>
<!-- Copyright 2013 Spraylight GmbH -->
<Package id="game">
    
    <!-- Animation resources -->
    <Resource id="anim_game_screen"     fileName="anim_game_screen.xml"/>
    
    <!-- Bitmap resources -->
    <Resource id="gfx_clubs"            fileName="gfx_clubs.png"/>
    <Resource id="planes_clubs"         fileName="planes_clubs.xml"/>
    <Resource id="gfx_diamonds"         fileName="gfx_diamonds.png"/>
    <Resource id="planes_diamonds"      fileName="planes_diamonds.xml"/>
    <Resource id="gfx_hearts"           fileName="gfx_hearts.png"/>
    <Resource id="planes_hearts"        fileName="planes_hearts.xml"/>
    <Resource id="gfx_misc"             fileName="gfx_misc.png"/>
    <Resource id="planes_misc"          fileName="planes_misc.xml"/>
    <Resource id="gfx_spades"           fileName="gfx_spades.png"/>
    <Resource id="planes_spades"        fileName="planes_spades.xml"/>
    
    <!-- Font resources -->
    <Resource id="arial_color_24_glyphs" fileName="fonts/arial_color_24_glyphs.murl"/>
    <Resource id="arial_color_24_map"    fileName="fonts/arial_color_24_map.png"/>
    <Resource id="arial_color_48_glyphs" fileName="fonts/arial_color_48_glyphs.murl"/>
    <Resource id="arial_color_48_map"    fileName="fonts/arial_color_48_map.png"/>
    
    <!-- Graph resources -->
    <Resource id="graph_camera"         fileName="graph_camera.xml"/>
    <Resource id="graph_game_card"      fileName="graph_game_card.xml"/>
    <Resource id="graph_game_card_suit" fileName="graph_game_card_suit.xml"/>
    <Resource id="graph_game_screen"    fileName="graph_game_screen.xml"/>
    <Resource id="graph_materials"      fileName="graph_materials.xml"/>
    <Resource id="graph_textures"       fileName="graph_textures.xml"/>
    
    <!-- Graph instances -->
    <Instance graphResourceId="graph_materials"/>
    <Instance graphResourceId="graph_textures"/>
    <Instance graphResourceId="graph_camera"/>
    
</Package>

The actual starting point for this is the graph_camera instance. This defines as usual a view with a camera and instantiates the graph_game_screen sub-graph. Dividing the sub-graphs on file level is recommended in order to keep track at larger projects.

<?xml version="1.0" ?>
<!-- Copyright 2013 Spraylight GmbH -->
<Graph>
    <Namespace id="game_screen">
        
        <Timeline id="screen_timeline">
            <Transform controller.animationResourceId="game:anim_game_screen">
                
                <FixedParameters id="screen_parameters"
                controller.animationResourceId="game:anim_game_screen"/>
                <ParametersState parametersId="screen_parameters"/>
                
                <!-- Green playfield -->
                <Reference targetId="/materials/state_plain_color"/>
                <FixedParameters diffuseColor="0i, 100i, 0i, 255i"
                    parentParametersId="screen_parameters">
                    <PlaneGeometry posX="0" posY="0"
                    frameSizeX="1500" frameSizeY="768"/>
                </FixedParameters>
                
                <!-- Text material -->
                <Reference targetId="/materials/state_plain_tex_color"/>
                
                <!-- Title text -->
                <Reference targetId="/textures/arial_color_48"/>
                <TextGeometry id="title_text"
                posX="0" posY="340"
                fontResourceId="game:arial_color_48_glyphs"
                text="Pyramid Card Game"/>
                
                <!-- Info text -->
                <Reference targetId="/textures/arial_color_24"/>
                <TextGeometry id="info_text"
                posX="0" posY="-140"
                fontResourceId="game:arial_color_24_glyphs"/>
                
                <!-- Cards -->
                <Reference targetId="/materials/state_plain_front_tex_color"/>
                
                <TextureState textureId="/textures/tex_misc"/>
                <Instance cardName="black_joker" graphResourceId="game:graph_game_card"/>
                <Instance cardName="red_joker"   graphResourceId="game:graph_game_card"/>
                
                <Instance suitName="clubs"    graphResourceId="game:graph_game_card_suit"/>
                <Instance suitName="diamonds" graphResourceId="game:graph_game_card_suit"/>
                <Instance suitName="hearts"   graphResourceId="game:graph_game_card_suit"/>
                <Instance suitName="spades"   graphResourceId="game:graph_game_card_suit"/>
                
                <Button id="stack_button" depthOrder="1000"/>
                
            </Transform>
        </Timeline>

    </Namespace>
</Graph>

Our playing screen starts with a <PlaneGeometry> node for a green background and two <TextGeometry> nodes to display text information.

The last block instantiates the playing cards. In order to display a card face and card back, we reference a material, which uses the so-called Backface Culling of the graphic chip. This feature only renders the card face of polygons and is activated in the corresponding <Material> node with the attribute visibleFaces="FRONT_ONLY".

Every card is instantiated with the following sub-graph. Our example begins with the instances of the black_joker and red_joker card.

It has to be considered that the suitable <TextureState> is set prior to this.

<?xml version="1.0" ?>
<!-- Copyright 2013 Spraylight GmbH -->
<Graph>
    
    <Namespace id="{cardName}" activeAndVisible="no">
        
        <Transform id="position">
            
            <Reference targetId="/textures/{cardName}"/>
            
            <SubState>
                <TextureState textureId="/textures/tex_misc"/>
                <Transform axisX="0" axisY="1" axisZ="0" angle="180 deg">
                    <Switch id="back_switch">
                        <Reference targetId="/textures/black_back"/>
                        <Reference targetId="/textures/red_back"/>
                    </Switch>
                </Transform>
            </SubState>
            
            <Button id="button" sizeX="90" sizeY="130"/>
            
        </Transform>
        
    </Namespace>
    
</Graph>

Here, the face and back side texture of the card instance is referenced. The texture of the back side is rotated through 180° on the vertical axis in order to fulfill the conditions of Backface Culling.

To create all instances of the four card suits, another sub-graph is instantiated which summarizes all cards with the same texture.

<?xml version="1.0" ?>
<!-- Copyright 2013 Spraylight GmbH -->
<Graph>
    
    <TextureState textureId="/textures/tex_{suitName}"/>
    
    <Instance cardName="ace_of_{suitName}"   graphResourceId="game:graph_game_card"/>
    <Instance cardName="2_of_{suitName}"     graphResourceId="game:graph_game_card"/>
    <Instance cardName="3_of_{suitName}"     graphResourceId="game:graph_game_card"/>
    <Instance cardName="4_of_{suitName}"     graphResourceId="game:graph_game_card"/>
    <Instance cardName="5_of_{suitName}"     graphResourceId="game:graph_game_card"/>
    <Instance cardName="6_of_{suitName}"     graphResourceId="game:graph_game_card"/>
    <Instance cardName="7_of_{suitName}"     graphResourceId="game:graph_game_card"/>
    <Instance cardName="8_of_{suitName}"     graphResourceId="game:graph_game_card"/>
    <Instance cardName="9_of_{suitName}"     graphResourceId="game:graph_game_card"/>
    <Instance cardName="10_of_{suitName}"    graphResourceId="game:graph_game_card"/>
    <Instance cardName="jack_of_{suitName}"  graphResourceId="game:graph_game_card"/>
    <Instance cardName="queen_of_{suitName}" graphResourceId="game:graph_game_card"/>
    <Instance cardName="king_of_{suitName}"  graphResourceId="game:graph_game_card"/>
    
</Graph>

Consequently, we have created all 54 card instances including their face and back with reasonable effort.

Application

The classic card game Pyramid Solitaire places 28 cards as three pyramids onto a tableau and uncovers the bottom row. The remaining cards are set on the tableau to form a draw pile – the waste. The top card of the waste is face up and any card of neighboring rank can be moved from the pyramids to the waste. The suit does not need to be considered. If no suitable card is on the pyramid, a new card can be revealed from the remaining cards.

To finish Pyramid Solitaire, all cards have to be eliminated from the tableau. The game is lost, if no suitable card can be moved to the waste and all remaining cards are revealed.

Setup of a card instance

Our game has 54 card instances, which are all controlled separately. For this purpose it is recommended to implement a class, which controls exactly one card. This class is then instantiated for each card. Therefore, it is possible to create a symmetry between the logic classes and the sub-graphs.

        class CardInstance : public Logic::GraphPositionInstance
        {
        public:
            CardInstance();
            virtual ~CardInstance();
            
            virtual Bool Init(Logic::INodeObserver* nodeObserver, const Graph::IRoot* root,
                              const String& path, SInt32 baseDepth);
            
            virtual void OnProcessTick(const Logic::IState* state);
            
            enum BackSideType
            {
                BLACK,
                RED
            };
            void SetBack(BackSideType backSide);
            
            void SetCardPosition(const Vector& position, Bool showFront, Real duration = 0);
            void MoveCardToPosition(const Vector& position, Bool showFront, Real duration);
            Bool IsMoving() const;
            Bool IsShowingFront() const;
            
            void EnableButton(Bool enable);
            Bool WasPressed() const;
            
        protected:
            Logic::ButtonNode mButton;
            Logic::SwitchNode mBackSwitch;
            
            Logic::BaseStepableObserver mStepableObserver;
            Logic::AnimationVector mPositionAnim;
            Logic::AnimationVector mRotationAnim;
            
            Bool mIsShowingFront;
        };

A card instance has a namespace node and a transform node. This typical constellation is supported by the Logic::GraphPositionInstance base class, which provides methods for manipulating the position and for activating the namespace node. The virtual Init(…) method is overloaded in order to initialize our derived class.

Furthermore, this base class is a stepable object allowing us to implement an OnProcessTick(…) method.

Afterwards, several methods are implemented in order to specify properties and movements and to query the current condition of the card.

The card instance has a Logic::ButtonNode mButton for interaction and a Logic::SwitchNode mBackSwitch node for the texture of the card back.

In order to animate the card, we use a Logic::AnimationVector mPositionAnim for positioning and a Logic::AnimationVector mRotationAnim for rotation.

Since the card object has no built-in StepableObserver, it has to be instantiated with Logic::BaseStepableObserver mStepableObserver.

Initialization of a card instance

Bool App::CardInstance::Init(Logic::INodeObserver* nodeObserver, const Graph::IRoot* root,
                             const String& path, SInt32 baseDepth)
{
    nodeObserver->Add(mButton.GetReference(root, path + "/button"));
    nodeObserver->Add(mBackSwitch.GetReference(root, path + "/back_switch"));
    
    if (!Logic::GraphPositionInstance::Init(nodeObserver, root, path, baseDepth))
    {
        return false;
    }
    
    mPositionAnim.AddKey(Real(0.0), Vector(Vector::ZERO_POSITION), IEnums::INTERPOLATION_EASE_OUT);
    mPositionAnim.AddKey(Real(1.0), Vector(Vector::ZERO_POSITION));
    
    mRotationAnim.AddKey(Real(0.0), Vector(), IEnums::INTERPOLATION_LINEAR);
    mRotationAnim.AddKey(Real(0.5), Vector(), IEnums::INTERPOLATION_EASE_OUT);
    mRotationAnim.AddKey(Real(1.0), Vector());
    
    mStepableObserver.Add(mPositionAnim);
    mStepableObserver.Add(mRotationAnim);
    
    SetBack(false);
    EnableButton(false);
    SetCardPosition(mPosition, false);
    mIsShowingFront = true;
    
    return true;
}

At first, the initialization creates the references for the button node and the switch node.

(!) Afterwards, the Init() method of the base class is called. This is absolutely necessary to ensure the functionality of the base class.

Furthermore, several animation data points are prepared and the AnimationVector instances are added to the StepableObserver.

In the last block, several default values are set.

Processing a card instance

void App::CardInstance::OnProcessTick(const Logic::IState* state)
{
    mStepableObserver.ProcessTick(state);
    
    if (mPositionAnim.IsOrWasRunning())
    {
        const Vector& currentPosition = mPositionAnim.GetCurrentValue();
        SetPosition(currentPosition);
    }
    
    if (mRotationAnim.IsOrWasRunning())
    {
        const Vector& currentRotation = mRotationAnim.GetCurrentValue();
        mTransformNode->SetRotation(currentRotation.x, currentRotation.y, currentRotation.z);
    }
}

(!) The OnProcessTick() method has to call the mStepableObserver.ProcessTick(state) method in order to make sure that the StepableObserver works properly.

Afterwards, the current values of the AnimationVector objects are evaluated and transferred to the corresponding card properties.

Logic Setup

        class CardGameLogic : public Logic::BaseProcessor
        {
            ...
            
            enum States
            {
                STATE_IDLE = 0,
                STATE_DEAL,
                STATE_PLAY
            };
            Logic::EnumStateMachine<States>::Type mStateMachine;
            
            void OnEnterDeal(const Logic::IState* logicState);
            void OnProcessTickDeal(const Logic::IState* logicState);
            
            void OnEnterPlay(const Logic::IState* logicState);
            void OnProcessTickPlay(const Logic::IState* logicState);
            
            ...
            
            ObjectArray<CardInstance> mCards;
            
            Util::TT800 mRng;
            UInt32Array mCardDistribution;
            SInt32Array mCardsToDeal;
            
            SInt32Array mCardStack;
            SInt32Array mCardTray;
            SInt32Array mPlayfield;
            
            Logic::BaseTimeframe mGameStartTimeout;
            Logic::BaseTimeframe mGameEndTimeout;
            UInt32 mDealCount;
            
            UInt32 mNumberOfGamesPlayed;
        };

Here, a part of the header is shown.

In order to simplify the implementation, the StateMachine class Logic::EnumStateMachine<States>::Type mStateMachine is used.

The card instances are created in the array ObjectArray<CardInstance> mCards.

(!) For objects the ObjectArray template class has to be used instead of the Array template class.

In order to easily measure timespans, a TimeFrame class is used: e.g. Logic::BaseTimeframe mGameStartTimeout.

Logic Initialization

Bool App::CardGameLogic::OnInit(const Logic::IState* state)
{
    state->GetLoader()->UnloadPackage("startup");
    
    const Graph::IRoot* root = state->GetGraphRoot();
    
    AddGraphNode(mScreenTimeline.GetReference(root, "/game_screen/screen_timeline"));
    AddGraphNode(mGameInfoText.GetReference(root, "/game_screen/info_text"));
    AddGraphNode(mStackButton.GetReference(root, "/game_screen/stack_button"));
    
    mCards.Empty();
    AddSuit(root, "clubs");
    AddSuit(root, "diamonds");
    AddSuit(root, "hearts");
    AddSuit(root, "spades");
    mCards.Add().Init(GetNodeObserver(), root, "/game_screen/black_joker", 0);
    mCards.Add().Init(GetNodeObserver(), root, "/game_screen/red_joker", 0);
    
    if (!AreGraphNodesValid())
    {
        return false;
    }
    
    mScreenTimeline->Start(Real(0.0), Real(0.5));
    
    for (UInt32 i = 0; i < mCards.GetCount(); i++)
    {
        AddStepable(mCards[i]);
    }
    
    AddStepable(mGameStartTimeout);
    AddStepable(mGameEndTimeout);
    
    mStateMachine.Register<CardGameLogic>(STATE_DEAL, this, &CardGameLogic::OnProcessTickDeal,
                                          &CardGameLogic::OnEnterDeal);
    mStateMachine.Register<CardGameLogic>(STATE_PLAY, this, &CardGameLogic::OnProcessTickPlay,
                                          &CardGameLogic::OnEnterPlay);
    AddStepable(mStateMachine);
    
    return true;
}

The first block initializes as usual several references to the corresponding nodes

The next block initializes the card instances and adds them to the mCards object array.

Afterwards, all references are checked and the mScreenTimeline is started to display the playing screen.

Furthermore, in the next block all card instances are added to the processor in order to also call the OnProcessTick() methods of the card instances in every logic step.

TimeFrame

The TimeFrame class is a stepable object and is added to the processor with AddStepable(mGameStartTimeout).

Timeframes can measure a certain timespan and signalize their course. The time measurement starts by calling the method Start(). The methods for querying the condition of the timeframe class are named as the methods of timeline classes, e.g. IsRunning(), WasRunning() etc.

StateMachine

The logic class StateMachine allows calling methods subject to a freely selectable condition.

Up to four methods can be assigned to one condition: An OnProcessTick(), an OnEnter(), an OnLeave() and an onFinishTick() method. Unused methods can be assigned to null-pointers.

The OnProcessTick() method of the current condition is called in every logic step. When the next condition is set, the OnLeave() method of the old condition and the OnEnter() method of the new condition are called.

In our tutorial, the methods OnProcessTickDeal() and OnEnterDeal() are assigned to the STATE_DEAL condition. The methods OnProcessTickPlay() and OnEnterPlay() are assigned to the STATE_PLAY condition. In our case, the STATE_IDLE is the initial condition and does not need a method.

The StateMachine class is a stepable object and is added with AddStepable(mStateMachine) to the processor.

Note
Tip: Stepable instances are processed in the added order. Therefore, it is recommended to add the StateMachine as last class since at this stage all other objects are already calculated.
Attention: Usually, in the processor stepables are processed before the OnProcessTick() method of the processor.

Logic Processing

void App::CardGameLogic::OnProcessTick(const Logic::IState* state)
{
    if (mScreenTimeline->WasRunning())
    {
        mStateMachine.SetNextState(STATE_DEAL);
    }

    Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler();
    if (deviceHandler->WasRawButtonPressed(RAWBUTTON_BACK))
    {
        deviceHandler->TerminateApp();
    }
}

Since the StateMachine is used the actual OnProcessTick() method is rather short.

At first, the StateMachine is set onto the STATE_DEAL as soon as the playing screen has been fully displayed.

The second block is only necessary for Android devices to actually close the application when pressing the back button.

Game Data Setup

The card instances are located in the ObjectArray mCards and remain there forever. The gameplay only saves indices, which refer to the card instances.

The gameplay only decides where and how and the card instances are shown.

The method ShuffleCards() creates with the RNG a random sequence of card indices in mCardsToDeal. These are used as follows:

The first 28 indices are moved to mPlayfield which represents the pyramid.

All other indices are moved to mCardStack which is the pile of the remaining cards.

During gameplay the corresponding indices are moved from mPlayfield or mCardStack to mCardTray which represents the revealed pile.

tut0203_card_game.png
Card Game output window

Processing of the application states

Bool App::CardGameApp::Configure(IEngineConfiguration* engineConfig, IFileInterface* fileInterface)
{
    const IPlatformConfiguration* platformConfig = engineConfig->GetPlatformConfiguration();
    IAppConfiguration* appConfig = engineConfig->GetAppConfiguration();
    
    engineConfig->SetProductName("CardGame");
    appConfig->SetWindowTitle("CardGame");
    
    if (platformConfig->IsTargetClassMatching(IEnums::TARGET_CLASS_COMPUTER))
    {
        if (platformConfig->IsOperatingSystemMatching(IEnums::OPERATING_SYSTEM_WINDOWS))
        {
            engineConfig->SetVideoApi(IEnums::VIDEO_API_DX9);
        }
        
        appConfig->SetDisplaySurfaceSize(1024, 768);
        appConfig->SetLockWindowAspectEnabled(true);
        appConfig->SetFullScreenEnabled(false);
    }
    
    if (platformConfig->IsTargetClassMatching(IEnums::TARGET_CLASS_HANDHELD))
    {
        appConfig->SetOrientationActive(true);
        appConfig->SetAutoRotationActive(true);
        appConfig->SetScreenOrientation(IEnums::SCREEN_ORIENTATION_LANDSCAPE_1);
        appConfig->SetAllowedScreenOrientations(appConfig->GetLandscapeOrientations());
        appConfig->SetMultiTouchActive(true);
    }
    
    engineConfig->SetDeactivatedAppRunState(IEnums::APP_RUN_STATE_PAUSED);
    
    return true;
}

When a window is activated or deactivated, the setup engineConfig->SetDeactivatedAppRunState(IEnums::APP_RUN_STATE_PAUSED) executes the following method:

void App::CardGameLogic::OnRunStateChanged(const Logic::IState* state, IEnums::AppRunState currentState,
                                           IEnums::AppRunState previousState)
{
    if (mGameStartTimeout.IsOrWasRunning())
    {
        return;
    }
    
    if (currentState == IEnums::APP_RUN_STATE_PAUSED)
    {
        mGameInfoText->SetText("- Paused -");
        if (mStateMachine.GetCurrentState() == STATE_PLAY)
        {
            UpdatePlayfield(Real(0.0), true);
        }
    }
    else if (currentState == IEnums::APP_RUN_STATE_RUNNING)
    {
        mGameInfoText->SetText("- Play -");
        if (mStateMachine.GetCurrentState() == STATE_PLAY)
        {
            UpdatePlayfield(Real(0.3), false);
        }
    }
}

This method covers the cards, when the window is deactivated, and reveals the cards as soon as the window is activated again.

If the application is paused as defined by the engine, the logic time stops and no OnProcessTick() is executed. The rendering, however, continues normally and the OnProcessFrame() methods are still called.

On mobile devices this mechanism works identically upon application shutdown. However, closed applications are paused by the current operating systems and hence no rendering and no OnProcessFrame() takes place.

On iOS devices the following entry has to be made in the info.plist of the application:

<key>UIApplicationExitsOnSuspend</key>
    <false/>

This entry is also called "Application does not run in background" in the editor.

Exercise

The game should remain paused, when the user returns to the running game. Furthermore, the user should be asked to click onto or touch the playing field in order to continue the game.

Debug vs. Release

Within the debug configuration we usually work directly with the resources in the corresponding directories, e.g. game.murlres. This has the advantage to be able to test modifications within the files during application development without previously starting the resource_packer.

In the release configuration, however, all resources are generated and a corresponding package is created with the resource_packer, e.g. game.murlpkg. The necessary scripts are called with an additional build step in the development environment in order to ensure that the current resources are in the packages as well.

In Xcode such additional build steps can only be set once per target and without being subject of the debug/release setting. For this reason, we need to create these two targets in our example:

  • CardGameDev Debug-Build configuration
  • CardGame Release-Build configuration with corresponding script build step.

Everything Step

Our tutorial shows that almost every object needs a certain timespan to present itself. This can be achieved by implementing the OnProcessTick() method.

Hence, it has to be ensured that every instance of an OnProcessTick() method is called exactly once in each logic step.

In our tutorial, we add several stepable objects to the processor. The largest objects are the 54 card instances which are added 54 times. Every card instance implements a StepableObserver, which has added the animation instances within the card instance.

(!) A good program should consider the need of OnProcessTick() calls already during draft and implementation phase.

(!) A typical error is, if stepable instances are either called multiple times or not at all.

For those interested, here the course per logic step is sketched:

CardGameLogic::Processor::ProcessTick()
CardGameLogic::StepableObserver::ProcessTick()
54 x CardInstance::Stepable::ProcessTick()
CardInstance::OnProcessTick()
CardInstance::mStepableObserver::ProcessTick()
CardInstance::mPositionAnim::LogicTimeline::ProcessTick()
CardInstance::mPositionAnim::LogicAnimation::OnEvaluate()
CardInstance::mRotationAnim::LogicTimeline::ProcessTick()
CardInstance:::mRotationAnim::LogicAnimation::OnEvaluate()
CardGameLogic::mGameStartTimeout::ProcessTick()
CardGameLogic::mGameEndTimeout::ProcessTick()
CardGameLogic::mStateMachine::ProcessTick()
CardGameLogic::mStateMachine::OnProcessTick()
CardGameLogic::On[ProcessTick][Enter]Deal() or CardGameLogic::On[ProcessTick][Enter]Play()
CardGameLogic::OnProcessTick()

Here, it can be seen that a ProcessTick() method is called internally at first. Afterwards, a corresponding method, e.g. OnProcessTick() or OnEvaluate(), is called.

(!) Basic rule: Hands off the ProcessTick() methods, if you do not exactly know what you are doing. Preferably, applications should work with OnProcessTick() methods and other On...() methods. It is possible that software updates in the engine change the behavior of methods. The behavior of On...() methods, however, always remains the same.


Copyright © 2011-2024 Spraylight GmbH.