Tutorial #04: Lua Card Game

Aufbauend auf dem vorherigen Beispiel Tutorial #03: Card Game wird hier demonstriert, wie das Card-Game ausschließlich mit Lua implementiert werden kann.

An dieser Stelle wird davon ausgegangen, dass entsprechende Vorkenntnisse im Bereich der Lua Programmierung vorhanden sind.

Zu beachten
Um Lua Skripte verwenden zu können, muss das optionale Lua Addon in das Projekt eingebunden werden. Am einfachsten geht das mit dem Murl Dashboard und dem Kommando Project / Configure Project.

Ressourcen Paket

Dieses Beispiel benutzt eine Kopie der Ressourcen game.murlres aus Tutorial #03.

Zusätzlich werden dem Paket Skript-Ressourcen in einem neuen Ordner lua hinzugefügt.

  • data/packages/game.murlres/lua/card_game_logic.lua
  • data/packages/game.murlres/lua/card_game_card_instance.lua

Die Skript Ressourcen müssen wie alle anderen Ressourcen in das Paket eingebunden werden.

<?xml version="1.0" ?>
<!-- Copyright 2014 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"/>
    
    <!-- Lua resources -->
    <Resource id="lua_card_game_logic"          fileName="lua/card_game_logic.lua" enableCompression="yes"/>
    <Resource id="lua_card_game_card_instance"  fileName="lua/card_game_card_instance.lua" enableCompression="yes"/>

    <!-- Graph instances -->
    <Instance graphResourceId="graph_materials"/>
    <Instance graphResourceId="graph_textures"/>
    <Instance graphResourceId="graph_camera"/>

    <!-- Script instances -->
    <Instance scriptResourceId="lua_card_game_logic"/>

</Package>

Die Ressource lua_card_game_logic wird mit <Instance scriptResourceId="lua_card_game_logic"/> im Paket instanziert. Dies hat zur Folge, dass das angegebene Skript nach dem Laden des Pakets während der Paket-Initialisierung automatisch als Logik-Prozessor eingebunden und wie jeder andere Logik-Prozessor ausgeführt wird.

Szenengraph

Das zweite Skript lua_card_game_card_instance wird als Skript Kontroller eines Graph-Knotens eingesetzt.

<?xml version="1.0" ?>
<!-- Copyright 2014 Spraylight GmbH -->
<Graph>
    
    <Namespace id="{cardName}" activeAndVisible="no"
        controller="ScriptLogicController"
        controller.scriptResourceId="game:lua_card_game_card_instance">

        <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>

Dies hat zur Folge, dass jede Karten Graph-Instanz eine eigene Instanz des lua_card_game_card_instance Skripts erhält, welche nach dem Initialisieren des Graph-Knotens automatisch bei jedem Logik-Tick ausgeführt wird.

Applikation

Eine reine Lua Applikation benutzt einen kleinen generischen C++ Programmteil, welcher zum Laden des ersten Lua Skripts dient. Dieses Skript muss eine Lua Implementierung der IApp-Klasse beinhalten und direkt im Paket Ordner liegen.

  • data/packages/lua_card_game_app.lua
Zu beachten
Tipp: Lua Projekte mit den entsprechenden Programmteilen lassen sich einfach mit dem Murl Dashboard erstellen wenn bei Create Project das Template Lua ausgewählt ist.

Der Lua Programmablauf ist nahezu identisch mit dem C++ Programm aus Tutorial #03.

-- Copyright 2014 Spraylight GmbH

-- Open Lua standard libraries
local luaAddon = Murl.Addons.Lua.Factory.GetAddon()
luaAddon:OpenLibrary("math")
luaAddon:OpenLibrary("string")
luaAddon:OpenLibrary("table")
luaAddon:OpenLibrary("utf8")

-- Parameter for Murl.IApp
local name = ...
-- print("\nLUA IApp new '" ..  name .. "'")

-- Murl IApp callbacks
local mainIApp =
{
    Configure = function (self, engineConfig, fileInterface)
        -- print("LUA IApp Configure")

        local appConfig = engineConfig:GetAppConfiguration()
        local platformConfig = engineConfig:GetPlatformConfiguration()

        engineConfig:SetProductName("LuaCardGame")
        appConfig:SetWindowTitle("LuaCardGame powered by Murl Engine")

        appConfig:SetSystemDebugInfoItems(Murl.IEnums.STATISTIC_ITEM_FRAMES_PER_SECOND)

        if (platformConfig:IsTargetClassMatching(Murl.IEnums.TARGET_CLASS_COMPUTER)) then

            if (platformConfig:IsOperatingSystemMatching(Murl.IEnums.OPERATING_SYSTEM_WINDOWS)) then
                engineConfig:SetVideoApi(Murl.IEnums.VIDEO_API_DX90)
            end

            appConfig:SetDisplaySurfaceSize(1024, 768)
            appConfig:SetLockWindowAspectEnabled(true)
            appConfig:SetFullScreenEnabled(false)

        elseif (platformConfig:IsTargetClassMatching(Murl.IEnums.TARGET_CLASS_HANDHELD)) then

            -- set landscape orientation
            appConfig:SetScreenOrientation(Murl.IEnums.SCREEN_ORIENTATION_LANDSCAPE_1)
            -- enable landscape orientations
            appConfig:SetAllowedScreenOrientations(Murl.IEnums.SCREEN_ORIENTATIONS_LANDSCAPE)
            -- enable auto rotation
            appConfig:SetAutoRotationActive(true)
            appConfig:SetOrientationActive(true)
            -- enable multi touch
            appConfig:SetMultiTouchActive(true)

        end

        engineConfig:SetDeactivatedAppRunState(Murl.IEnums.APP_RUN_STATE_PAUSED)

        return true
    end,

    IsUserConfigurationMatching = function (self, userConfigId)
        -- print("LUA IApp IsUserConfigurationMatching", userConfigId)

        return false
    end,

    RegisterCustomAddonClasses = function (self, addonRegistry)
        -- print("LUA IApp RegisterCustomAddonClasses")

        return true
    end,

    UnregisterCustomAddonClasses = function (self, addonRegistry)
        -- print("LUA IApp UnregisterCustomAddonClasses")

        return true
    end,

    Init = function (self, appState)
        -- print("LUA IApp Init")

        local loader = appState:GetLoader()
        loader:AddPackage("startup", Murl.ILoader.LOAD_MODE_STARTUP)
        loader:AddPackage("game", Murl.ILoader.LOAD_MODE_BACKGROUND)

        return true
    end,

    DeInit = function (self, appState)
        -- print("\nLUA IApp DeInit")

        return true
    end
}

return Murl.IApp.new(mainIApp)

Dieses Skript wird unmittelbar nach dem Programmstart geladen und ausgeführt. Als Rückgabewert wird ein IApp Objekt erwartet.

  • Im ersten Block (Zeile 4) des Programmcodes werden diverse Lua Standard-Bibliotheken geladen.
  • Im zweiten Block (Zeile 11) werden die Parameter ausgewertet. Das Lua Applikations-Skript bekommt genau einen Parameter mit dem Namen der Skript-Ressource.
  • Im dritten Block (Zeile 15) wird eine Lua Tabelle mit Funktionen erzeugt. Diese Funktionen fungieren als Callbacks und haben dieselbe Funktion wie die gleichnamigen C++ IApp-Methoden.
  • An letzter Stelle (Zeile 92) wird eine Instanz der IApp-Klasse, welche die Lua Callback Tabelle beinhaltet, erzeugt und zurückgegeben.

Im Anschluss werden die Lua Callback Funktionen in mainIApp genauso ausgeführt, wie normalerweise die gleichnamigen C++ Methoden.

Die Funktion Configure() (Zeile 17) beinhaltet den gleichen Programmablauf wie die C++ Methode in card_game_app.cpp aus Tutorial #03. Selbiges gilt für die Funktion Init() (Zeile 75) welche die Ressourcen-Pakete lädt.

Die Programmierung in Lua unterscheidet sich syntaktisch geringfügig von C++, jedoch sind weitgehend alle Murl Klassen auch in gleichnamigen Lua Tabellen abgebildet. Eine vollständige Dokumentation aller Lua Tabellen findet sich in der Download Sektion im MurlEngineLuaAPI Archiv.

Skript einer Karteninstanz

Unser Spiel hat 54 Karteninstanzen, welche alle separat gesteuert werden. Um dies möglichst einfach zu erreichen, wird ein Skript implementiert, welches genau eine Karte steuert. Dieses Skript wird wie weiter oben gezeigt im Sub-Graphen graph_game_card.xml als Controller eingebunden.

-- Copyright 2014 Spraylight GmbH

-- Parameters for Logic IAppGraph
local resourceId, nodeId = ...
-- print("\nLUA IAppGraph new", resourceId, nodeId)

-- Logic IAppGraph callbacks
local logicIAppGraph =
{
    mNodeObserver = Murl.Logic.StaticFactory.CreateNodeObserver(),
    mNamespaceNode = Murl.Logic.NamespaceNode.new(),
    mTransformNode = Murl.Logic.TransformNode.new(),
    mButton = Murl.Logic.ButtonNode.new(),
    mBackSwitch = Murl.Logic.SwitchNode.new(),

    mStepableObserver = Murl.Logic.StaticFactory.CreateStepableObserver(),
    mPositionAnim = Murl.Logic.AnimationVector.new(),
    mRotationAnim = Murl.Logic.AnimationVector.new(),

    mIsShowingFront = false,

    OnPostInit = function (self, state)
        -- print("LUA IAppGraph OnInit", nodeId)

        self.mNodeObserver:Add(self.mNamespaceNode:GetReference(state:GetCurrentGraphNode()))
        if (not self.mNamespaceNode:IsValid()) then
            return false
        end

        self.mNodeObserver:Add(self.mTransformNode:GetReference(self.mNamespaceNode:FindNode("position")))

        self.mNodeObserver:Add(self.mButton:GetReference(self.mNamespaceNode:FindNode("button")))
        self.mNodeObserver:Add(self.mBackSwitch:GetReference(self.mNamespaceNode:FindNode("back_switch")))

        if (not self.mNodeObserver:AreValid()) then
            return false
        end

        self.mPositionAnim:AddKey(0.0, Murl.Math.Vector.new(Murl.Math.Vector.ZERO_POSITION), Murl.IEnums.INTERPOLATION_EASE_OUT)
        self.mPositionAnim:AddKey(1.0, Murl.Math.Vector.new(Murl.Math.Vector.ZERO_POSITION))

        self.mRotationAnim:AddKey(0.0, Murl.Math.Vector.new(), Murl.IEnums.INTERPOLATION_LINEAR)
        self.mRotationAnim:AddKey(0.5, Murl.Math.Vector.new(), Murl.IEnums.INTERPOLATION_EASE_OUT)
        self.mRotationAnim:AddKey(1.0, Murl.Math.Vector.new());

        self.mStepableObserver:Add(self.mPositionAnim:GetStepable())
        self.mStepableObserver:Add(self.mRotationAnim:GetStepable())

        self:SetBack(CARDBACKSIDE_BLACK)
        self:EnableButton(false)
        self:SetCardPosition(self.mTransformNode:GetPosition(), false, 0)
        self.mIsShowingFront = true

        return true
    end,

    OnPreDeInit = function (self, state)
        -- print("LUA IAppGraph OnDeInit", nodeId)
        local ret = true

        if (not self.mNodeObserver:RemoveAll()) then
            ret = false
        end

        return ret
    end,

    OnPreProcessTick = function (self, state)
        self.mStepableObserver:ProcessTick(state)

        if (self.mPositionAnim:IsOrWasRunning()) then
            self.mTransformNode:SetPosition(self.mPositionAnim:GetCurrentValue())
        end

        if (self.mRotationAnim:IsOrWasRunning()) then
            local currentRotation = self.mRotationAnim:GetCurrentValue()
            self.mTransformNode:SetRotation(currentRotation.x, currentRotation.y, currentRotation.z)
        end
    end,

    SetObtained = function (self, isObtained)
        self.mNamespaceNode:GetNodeInterface():SetActiveAndVisible(isObtained)
    end,

    SetSortDepth = function (self, sortDepth)
        self.mTransformNode:SetDepthOrder(sortDepth)
    end,

    SetBack = function (self, backSide)
        if (backSide == CARDBACKSIDE_RED) then
            self.mBackSwitch:SetIndex(1)
        else
            self.mBackSwitch:SetIndex(0)
        end
    end,

    SetCardPosition = function (self, position, showFront, duration)
        self.mTransformNode:SetPosition(position)
        if (showFront) then
            if (not self.mIsShowingFront) then
                if (duration > 0) then
                    self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, -Murl.Math.PI, 0, 0))
                    self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0, -Murl.Math.HALF_PI, 0, 0))
                    self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, 0, 0, 0))
                    self.mRotationAnim:ModifyKeyTime(1, duration / 2)
                    self.mRotationAnim:ModifyKeyTime(2, duration)
                    self.mRotationAnim:StartForward()
                else
                    self.mTransformNode:SetRotation(0, 0, 0)
                end
            end
        else
            if (self.mIsShowingFront) then
                if (duration > 0) then
                    self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, 0, 0, 0))
                    self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0, Murl.Math.HALF_PI, 0, 0))
                    self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, Murl.Math.PI, 0, 0))
                    self.mRotationAnim:ModifyKeyTime(1, duration / 2)
                    self.mRotationAnim:ModifyKeyTime(2, duration)
                    self.mRotationAnim:StartForward()
                else
                    self.mTransformNode:SetRotation(0, Murl.Math.PI, 0)
                end
            end
        end
        self.mIsShowingFront = showFront
    end,

    MoveCardToPosition = function (self, position, showFront, duration)
        self.mPositionAnim:SetKey(0, 0.0, self.mTransformNode:GetPosition(), Murl.IEnums.INTERPOLATION_EASE_OUT)
        self.mPositionAnim:SetKey(1, duration, position)
        self.mPositionAnim:StartForward()

        if (showFront) then
            if (mIsShowingFront) then
                self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, 0, 0, 0))
                self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0, -1.0, 0.25, 0))
                self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, 0, 0, 0))
            else
                self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, Murl.Math.PI, 0, 0))
                self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0, Murl.Math.HALF_PI, 0.5, 0))
                self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, 0, 0, 0))
            end
        else
            if (mIsShowingFront) then
                self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, 0, 0, 0))
                self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0, Murl.Math.HALF_PI, 0.5, 0))
                self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, Murl.Math.PI, 0, 0))
            else
                self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, Murl.Math.PI, 0, 0))
                self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0.5, Murl.Math.PI - 0.5, 0.5, 0))
                self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, Murl.Math.PI, 0, 0))
            end
        end
        self.mRotationAnim:ModifyKeyTime(1, duration / 2)
        self.mRotationAnim:ModifyKeyTime(2, duration)
        self.mRotationAnim:StartForward()
        self.mIsShowingFront = showFront
    end,

    IsMoving = function (self)
        return self.mPositionAnim:IsOrWasRunning()
    end,

    EnableButton = function (self, enable)
        self.mButton:SetEnabled(enable)
    end,

    WasPressed = function (self)
        return self.mButton:WasPressed()
    end,
}

-- add this instance to the global card instances table
table.insert(mCards, logicIAppGraph)

return Murl.Logic.IAppGraph.new(logicIAppGraph)

Dieses Skript wird beim Erzeugen des zugehörigen Graph-Knotens geladen und ausgeführt. Als Rückgabewert wird ein Logic.IAppGraph Objekt erwartet.

  • Im ersten Block (Zeile 4) werden die Parameter ausgewertet, das Lua Kontroller Skript bekommt genau zwei Parameter. Der erste Parameter ist der Name der Skript-Ressource und der zweite Parameter ist der Name des Kontroller-Knotens.
  • Im zweiten Block (Zeile 8) wird eine Lua Tabelle mit Funktionen erzeugt. Diese Funktionen fungieren als Callbacks und haben dieselbe Funktion wie die gleichnamigen C++ Logic::IAppGraph Methoden.
  • Im dritten Block (Zeile 175) wird die Callback-Tabelle einer globalen Tabelle mCards hinzugefügt, von wo aus später der Logik Programmteil auf die einzelnen Karten Instanzen zugreifen kann.
  • An letzter Stelle (Zeile 177) wird eine Instanz der Logic.IAppGraph-Klasse, welche die Lua Callback Tabelle beinhaltet, erzeugt und retourniert.

Die Funktionen und Variablen der logic.IAppGraph Tabelle entsprechen den Methoden und Membervariablen von card_game_card_instance.cpp aus Tutorial #03.

Skript Prozessor

Die Logik-Prozessor Instanz wird wie weiter oben gezeigt direkt in package.xml instanziert.

-- Copyright 2014 Spraylight GmbH

-- Parameters for Logic IAppProcessor
local resourceId, replication = ...
-- print("\nLUA IAppProcessor new", resourceId, replication)

-- the global table holding all card instances
mCards = {}

CARDBACKSIDE_BLACK = 0
CARDBACKSIDE_RED = 1

-- Logic IAppProcessor callbacks
local logicIAppProcessor =
{
    STATE_IDLE = 0,
    STATE_DEAL = 1,
    STATE_PLAY = 2,
    mStateMachine = 0,

    mCardsPerSuit = 13,
    mCardSizeX = 90,
    mCardSizeY = 130,

    mScreenTimeline = Murl.Logic.TimelineNode.new(),
    mGameInfoText = Murl.Logic.TextGeometryNode.new(),
    mStackButton = Murl.Logic.ButtonNode.new(),

    mRng = Murl.Util.TT800.new(),
    mCardDistribution = Murl.UInt32Array.new(),
    mCardsToDeal = Murl.SInt32Array.new(),

    mCardStack = Murl.SInt32Array.new(),
    mCardTray = Murl.SInt32Array.new(),
    mPlayfield = Murl.SInt32Array.new(),

    mGameStartTimeout = Murl.Logic.Timeframe.new(),
    mGameEndTimeout = Murl.Logic.Timeframe.new(),

    mDealCount = 0,
    mNumberOfGamesPlayed = 0,

    OnInit = function (self, state)
        -- print("\nLUA IAppProcessor OnInit", resourceId)

        state:GetLoader():UnloadPackage("startup")

        local root = state:GetGraphRoot()
        local processor = state:GetCurrentProcessor()

        processor:AddGraphNode(self.mScreenTimeline:GetReference(root, "/game_screen/screen_timeline"))
        processor:AddGraphNode(self.mGameInfoText:GetReference(root, "/game_screen/info_text"))
        processor:AddGraphNode(self.mStackButton:GetReference(root, "/game_screen/stack_button"))

        if (not processor:AreGraphNodesValid()) then
            return false
        end

        self.mScreenTimeline:Start(0.0, 0.5)

        processor:AddStepable(self.mGameStartTimeout:GetStepable())
        processor:AddStepable(self.mGameEndTimeout:GetStepable())

        return true
    end,

    OnDeInit = function (self, state)
        -- print("\nLUA IAppProcessor OnDeInit", resourceId)
        local ret = true

        return ret
    end,

    OnProcessTick = function (self, state)
        if (self.mScreenTimeline:WasRunning()) then
            self:EnterDeal(state)
        end

        local deviceHandler = state:GetDeviceHandler()
        if (deviceHandler:WasRawButtonPressed(Murl.RAWBUTTON_BACK)) then
            deviceHandler:TerminateApp()
        end

        --print("State", self.mStateMachine)
        if (self.mStateMachine == self.STATE_DEAL) then
            self:ProcessTickDeal(state)
        elseif (self.mStateMachine == self.STATE_PLAY) then
            self:ProcessTickPlay(state)
        end
    end,

    OnRunStateChanged = function (self, state, currentState, previousState)
        -- print("\nLUA IAppProcessor OnRunStateChanged", resourceId)

        if (self.mGameStartTimeout:IsOrWasRunning()) then
            return
        end

        if (currentState == Murl.IEnums.APP_RUN_STATE_PAUSED) then
            self.mGameInfoText:SetText("- Paused -")
            if (self.mStateMachine == self.STATE_PLAY) then
                self:UpdatePlayfield(0.0, true)
            end
        elseif (currentState == Murl.IEnums.APP_RUN_STATE_RUNNING) then
            self.mGameInfoText:SetText("- Play -")
            if (self.mStateMachine == self.STATE_PLAY) then
                self:UpdatePlayfield(0.3, false)
            end
        end
    end,

    EnterDeal = function (self, state)
        -- print("\nLUA IAppProcessor EnterDeal", resourceId)
        self.mStateMachine = self.STATE_DEAL
        self.mNumberOfGamesPlayed = self.mNumberOfGamesPlayed + 1

        self:ShuffleCards(self.mNumberOfGamesPlayed % 3)
        self.mDealCount = 0
        self.mGameStartTimeout:Start(1.0)
        self.mGameInfoText:SetText("")
    end,

    ProcessTickDeal = function (self, state)
        -- print("LUA IAppProcessor ProcessTickDeal", resourceId)
        if (self.mGameStartTimeout:IsRunning()) then
            return
        end

        if (self.mCardsToDeal:GetCount() > 0) then
            local index = self.mCardsToDeal:Pop()
            local n = self.mCardStack:GetCount()
            self.mCardStack:Add(index)
            mCards[index + 1]:SetObtained(true)
            mCards[index + 1]:SetSortDepth(n + 1)
            mCards[index + 1]:SetCardPosition(self:GetStackPosition(n), false, 0)
            mCards[index + 1]:EnableButton(false)
            if ((self.mNumberOfGamesPlayed % 2) == 0) then
                mCards[index + 1]:SetBack(CARDBACKSIDE_RED)
            else
                mCards[index + 1]:SetBack(CARDBACKSIDE_BLACK)
            end
        elseif (self.mDealCount < self.mPlayfield:GetCount()) then
            local index = self.mCardStack:Pop()
            local showFront = (self.mDealCount >= 18)
            mCards[index + 1]:SetSortDepth(self.mDealCount + 1000)
            mCards[index + 1]:MoveCardToPosition(self:GetPlayfieldPosition(self.mDealCount), showFront, 0.5)
            mCards[index + 1]:EnableButton(showFront)
            self.mPlayfield[self.mDealCount] = index
            self.mGameStartTimeout:Start(0.1)
            self.mDealCount = self.mDealCount + 1
        else
            self:MoveToTray(self.mCardStack:Pop())
            self:UpdateStackButton()
            self:EnterPlay(state)
        end
    end,

    EnterPlay = function (self, state)
        -- print("\nLUA IAppProcessor EnterPlay", resourceId)
        self.mStateMachine = self.STATE_PLAY
        self.mGameInfoText:SetText("- Play -")
    end,

    ProcessTickPlay = function (self, state)
        -- print("LUA IAppProcessor ProcessTickPlay", resourceId)
        if (self.mGameEndTimeout:IsRunning()) then
            return
        end
        if (self.mGameEndTimeout:WasRunning()) then
            self:EnterDeal(state)
            return
        end

        local canAnyMoveToTray = false
        local isPlayfieldEmpty = true
        for i, index in Murl.ipairs(self.mPlayfield) do
            if (index >= 0) then
                isPlayfieldEmpty = false
                if (mCards[index + 1].mIsShowingFront) then
                    local canMoveToTray = self:CanMoveToTray(index)
                    if (canMoveToTray) then
                        canAnyMoveToTray = true
                    end
                    if (mCards[index + 1]:WasPressed()) then
                        if (canMoveToTray) then
                            self:MoveToTray(index)
                            self.mPlayfield[i] = -1
                            self:UpdatePlayfield(0.3, false)
                        end
                    end
                end
            end
        end

        if (isPlayfieldEmpty) then
            -- Pyramid complete
            self.mGameInfoText:SetText("Congratulations!")
            self.mGameEndTimeout:Start(3.0)
        elseif (self.mCardStack:GetCount() > 0) then
            if (self.mStackButton:WasPressed()) then
                self:MoveToTray(self.mCardStack:Pop())
                self:UpdateStackButton()
            end
        else
            if (not canAnyMoveToTray) then
                -- No more moves
                self.mGameInfoText:SetText("Game Over")
                self.mGameEndTimeout:Start(3.0)
            end
        end
    end,

    UpdateStackButton = function (self)
        local additionalSizeX = self.mCardStack:GetCount() * 4
        if (additionalSizeX > 0) then
            self.mStackButton:SetScaleFactorX(self.mCardSizeX + additionalSizeX)
            self.mStackButton:SetScaleFactorY(self.mCardSizeY + 4)
            local position = self:GetStackPosition(0)
            position.x = position.x + (additionalSizeX / 2)
            self.mStackButton:GetTransformInterface():SetPosition(position)
        end
    end,

    GetStackPosition = function (self, index)
        local position = Murl.Math.Vector.new(Murl.Math.Vector.ZERO_POSITION)
        position.x = -400 + (index * 4)
        position.y = -260
        return position
    end,

    GetTrayPosition = function (self, index)
        local position = Murl.Math.Vector.new(Murl.Math.Vector.ZERO_POSITION)
        position.x = 200 + (index * 2)
        position.y = -260
        return position
    end,

    GetPlayfieldPosition = function (self, index)
        local position = Murl.Math.Vector.new(Murl.Math.Vector.ZERO_POSITION)
        local DISTANCE_X = self.mCardSizeX + 10
        local DISTANCE_Y = self.mCardSizeY - 50

        if (index < 3) then
            position.x = (index * 3) * DISTANCE_X - (6 * DISTANCE_X / 2)
            position.y = DISTANCE_Y + DISTANCE_Y / 2
        elseif (index < 9) then
            position.x = (index - 3) * DISTANCE_X - (7 * DISTANCE_X / 2)
            position.x = position.x + (Murl.Math.Floor((index - 3) / 2) * DISTANCE_X)
            position.y = DISTANCE_Y / 2
        elseif (index < 18) then
            position.x = (index - 9) * DISTANCE_X - (8 * DISTANCE_X / 2)
            position.y = -DISTANCE_Y / 2
        else
            position.x = (index - 18) * DISTANCE_X - (9 * DISTANCE_X / 2)
            position.y = -DISTANCE_Y - DISTANCE_Y / 2
        end

        position.y = position.y + 100
        return position
    end,

    IsCardFree = function (self, index)
        -- Playfield index
        --        0           1           2
        --      3   4       5   6       7   8
        --    9  10  11  12  13  14  15  16  17
        -- 18  19  20  21  22  23  24  25  26  27

        local left = -1
        if (index < 3) then
            left = (index * 2) + 3
        elseif (index < 9) then
            left = (Murl.Math.Floor((index - 3) / 2)) + index + 6
        elseif (index < 18) then
            left = index + 9
        end

        local right = -1
        if (left >= 0) then
            right = left + 1
        end

        local free = true
        if (left >= 0 and left < self.mPlayfield:GetCount()) then
            if (self.mPlayfield[left] >= 0) then
                free = false
            end
        end
        if (right >= 0 and right < self.mPlayfield:GetCount()) then
            if (self.mPlayfield[right] >= 0) then
                free = false
            end
        end
        return free
    end,

    CanMoveToTray = function (self, index)
        local isValid = false
        local trayCard = self.mCardTray:Top()
        if (trayCard >= 4 * self.mCardsPerSuit or index >= 4 * self.mCardsPerSuit) then
            -- Joker
            isValid = true
        else
            local playCard = index % self.mCardsPerSuit
            local trayCardPlus = (trayCard + 1) % self.mCardsPerSuit
            local trayCardMinus = (trayCard + self.mCardsPerSuit - 1) % self.mCardsPerSuit
            if (playCard == trayCardPlus or playCard == trayCardMinus) then
                isValid = true
            end
        end
        return isValid
    end,

    MoveToTray = function (self, index)
        local n = self.mCardTray:GetCount()
        self.mCardTray:Add(index)
        mCards[index + 1]:SetSortDepth(n + 2000)
        mCards[index + 1]:MoveCardToPosition(self:GetTrayPosition(n), true, 0.5)
        mCards[index + 1]:EnableButton(false)
    end,

    UpdatePlayfield = function (self, duration, isPause)
        for i, index in Murl.ipairs(self.mPlayfield) do
            if (index >= 0) then
                local isFree = false
                if (not isPause) then
                    isFree = self:IsCardFree(i)
                end
                local card = mCards[index + 1]
                card:SetSortDepth(i + 1000)
                card:SetCardPosition(self:GetPlayfieldPosition(i), isFree, duration)
                if (isFree) then
                    card:EnableButton(true)
                end
            end
        end
    end,

    ShuffleCards = function (self, numberOfJokers)
        for _, card in ipairs(mCards) do
            card:SetObtained(false)
        end

        if (numberOfJokers > 2) then
            numberOfJokers = 2
        end
        self.mCardDistribution:Empty()
        self.mCardDistribution:SetCount(#mCards - 2 + numberOfJokers, 1)

        self.mCardsToDeal:Empty()
        for _ in Murl.ipairs(self.mCardDistribution) do
            local index
            index, self.mCardDistribution = self.mRng:DrawNoReplacement(self.mCardDistribution)
            self.mCardsToDeal:Add(index)
        end

        self.mCardStack:Empty()
        self.mCardTray:Empty()
        self.mPlayfield:Empty()
        self.mPlayfield:SetCount(28, -1)
    end
}

return Murl.Logic.IAppProcessor.new(logicIAppProcessor)

Dieses Skript wird beim Erzeugen des zugehörigen Ressourcen-Pakets geladen und ausgeführt. Als Rückgabewert wird ein Logic.IAppProcessor Objekt erwartet.

  • Im ersten Block (Zeile 4) werden die Parameter ausgewertet. Das Lua Prozessor Skript bekommt genau zwei Parameter. Der erste Parameter ist der Name der Skript-Ressource und der zweite Parameter ist die Replikations-Nummer.
  • Im zweiten Block (Zeile 14) wird eine Lua Tabelle mit Funktionen erzeugt. Diese Funktionen fungieren als Callbacks und haben dieselbe Funktion wie die gleichnamigen C++ Logic::IAppProcessor Methoden.
  • An letzter Stelle (Zeile 364) wird eine Instanz der Logic.IAppProcessor-Klasse, welche die Lua Callback Tabelle beinhaltet, erzeugt und retourniert.

Die Funktionen und Variablen der logicIAppProcessor Tabelle entsprechen den Methoden und Membervariablen von card_game_logic.cpp aus Tutorial #03.

Der auffälligste Unterschied zu der C++ Implementierung besteht in der Initialisierung der Karten-Instanzen, welche durch Verwendung des Skript-Kontrollers automatisch bei der Erstellung der Graph-Knoten stattfindet.

tut0204_lua_card_game.png
Lua Card Game Ausgabe Fenster


Copyright © 2011-2018 Spraylight GmbH.