How to create a custom FlowGraph node in C++ Part 1

#2
Introducing our new section dedicated to featuring community created tutorials, tips & tricks and code!
    We'll be using this thread to announce every new addition to our blog and to share any code samples and extra materials needed to complete the tutorials.
      This week 'ochounos' shows you how to create a custom FlowGraph node in C++!
        Read the full tutorial in our blog.
        Attachments
        FlowGraphSumNode.zip
        (1.1 KiB) Downloaded 26 times

        How to create a custom FlowGraph node in C++ Part 2

        #3
        How to create a custom FlowGraph node in C++ Part 2

        Be sure you checked out the first part of this tutorial which covers the basics of custom FlowGraph nodes.

        Sometimes you must control mechanics or gameplay every frame.

        In this tutorial we will create a node that:
          1. is updated every frame.
          2. sends ticks to other nodes in a specified timer interval.
          3. can be started, paused and stopped at any time.

        Let’s define the basics requirements for our custom node in our .cpp file:

        Code: Select all

        class CFlowTimerNode : public CFlowBaseNode<eNCT_Instanced>
        {
            bool paused;            // Node is paused or running

            int64 end_time;         // When node finishes emitting ticks
            int64 next_tick_time;   // Time when next tick must be emitted
            int64 interval_time;    // Interval time between ticks
            int64 paused_time;      // Time when node was paused

        public:

            // Default constructor
            CFlowTimerNode( SActivationInfo * pActInfo )
            {
                paused = false;
                end_time = next_tick_time = interval_time = paused_time = 0;
            }
                ...
        }

        // Register node with FlowGraph system
        REGISTER_FLOW_NODE( "MyCategory:Timer", CFlowTimerNode);

        Next we add all the inputs and outputs needed to our CFlowTimerNode class:

        Code: Select all

        enum EInPorts
        {
            IN_START = 0,
            IN_PAUSE,
            IN_STOP,
            IN_TIME,
            IN_INTERVAL,
        };
        enum EOutPorts
        {
            OUT_TICK,
            OUT_ONSTART,
            OUT_ONPAUSE,
            OUT_ONRESUME,
            OUT_ONSTOP,
            OUT_ONEND,
        };

        // Define the inputs and outputs of the node
        virtual void GetConfiguration( SFlowNodeConfig& config )
        {
            // Input ports
            static const SInputPortConfig in_config[] = {
                InputPortConfig<SFlowSystemVoid>( "Start", _HELP("Start emmiting ticks") ),
                InputPortConfig<SFlowSystemVoid>("Pause", _HELP("Stop/start the node")),
                InputPortConfig<SFlowSystemVoid>("Stop", _HELP("Stops emmiting ticks")),
                InputPortConfig<float>( "Time", _HELP("Time in seconds") ),
                InputPortConfig<float>( "Interval", _HELP("Interval in seconds between ticks") ),
                {0}
            };
            // Output ports
            static const SOutputPortConfig out_config[] = {
                OutputPortConfig<SFlowSystemVoid>( "Tick", _HELP("Active every interval secs") ),
                OutputPortConfig<SFlowSystemVoid>("OnStart", _HELP("Thrown when starts")),
                OutputPortConfig<SFlowSystemVoid>("OnPause", _HELP("Thrown when paused")),
                OutputPortConfig<SFlowSystemVoid>("OnResume", _HELP("Thrown when resume")),
                OutputPortConfig<SFlowSystemVoid>("OnStop", _HELP("Thrown when stops")),
                OutputPortConfig<SFlowSystemVoid>("OnEnd", _HELP("Thrown when runs out of time")),
                {0}
            };
            // Other configuration
            config.sDescription = _HELP("Emmit ticks at intervals");
            config.pInputPorts = in_config;
            config.pOutputPorts = out_config;
            config.SetCategory(EFLN_APPROVED);
        }


        In this node we will manage two different event types. We will receive the event eFE_Activate when any input is activated. There we can manage the actions to take. Later, we could receive the event eFE_Update every time that a frame is rendered. To achieve it we need to use the following function:

        Code: Select all

        pActInfo->pGraph->SetRegularlyUpdated(pActInfo->myID, true);


        SetRegularlyUpdated can be called inside of our inherited ProcessEvent function which we will cover next. It controls if the node specified by myID is to receive an update notification every frame (true) or not (false) for the flowgraph that pGraph point to.
        Next we will add logic to handle activity on the IN_START, IN_PAUSE and IN_STOP input port .
          • IN_START:
            • Add node to per frame update using SetRegularlyUpdated(…,true)
            • Calculate tick ratio and last tick time
          • IN_PAUSE:
            • Toggle pause status:
            • Paused:
            • Remove node from per frame update using SetRegularlyUpdated(…,false).
            • Activate output OUT_ONPAUSE
            • Resumed:
            • Add node to per frame update using SetRegularlyUpdated(…,true).
            • Activate output OUT_ONRESUME
          • IN_STOP:
            • Remove node from per frame update SetRegularlyUpdated(…,false).
            • Activate output OUT_ONSTOP

        Code: Select all

        virtual void ProcessEvent( EFlowEvent event, SActivationInfo *pActInfo )
        {
            // Every time that one input is modified the node receives an event
            if (event == eFE_Activate)
            {
                if (IsPortActive(pActInfo, IN_START))
                {
                    const int64 MILLISECS = 1000; // Used to convert seconds to milliseconds
                    int64 current_time = gEnv->pTimer->GetFrameStartTime().GetMilliSecondsAsInt64();
                    end_time = current_time + (int64)(GetPortFloat(pActInfo, IN_TIME) * MILLISECS);
                    interval_time = (int64)(GetPortFloat(pActInfo, IN_INTERVAL) * MILLISECS);
                    next_tick_time = current_time + interval_time;
                    // Start receiving events
                    pActInfo->pGraph->SetRegularlyUpdated(pActInfo->myID, true);
                    ActivateOutput(pActInfo, OUT_ONSTART, true);
                }

                if (IsPortActive(pActInfo, IN_PAUSE))
                {
                    int64 current_time = gEnv->pTimer->GetFrameStartTime().GetMilliSecondsAsInt64();
                    if (!paused)
                    {
                        // Stop temporary the node
                        paused_time = current_time;
                        pActInfo->pGraph->SetRegularlyUpdated(pActInfo->myID, false);
                        ActivateOutput(pActInfo, OUT_ONPAUSE, true);
                    }
                    else
                    {
                        // Continue the node
                        int64 diff_time = current_time - paused_time;
                        end_time += diff_time;
                        next_tick_time += diff_time;
                        pActInfo->pGraph->SetRegularlyUpdated(pActInfo->myID, true);
                        ActivateOutput(pActInfo, OUT_ONRESUME, true);
                    }
                    // Toggle the paused value
                    paused = !paused;
                }

                if (IsPortActive(pActInfo, IN_STOP))
                {
                    // Stop receiving more events
                    pActInfo->pGraph->SetRegularlyUpdated(pActInfo->myID, false);
                    ActivateOutput(pActInfo, OUT_ONSTOP, true);
                }
            }

            // Receive this event every frame if updated regularly is active
            if (event == eFE_Update)
            {
                // ...
            }
        }

        When the node is paused, we should stop receiving notifications and take current time for for future reference. If the node was already paused, we must update the time variables end_time and next_tick_time and start receiving notifications again.

        Finally, we need to manage what happen every time that we receive an update and the node is started and not paused. First, we will check if the node has not surpassed the end_time to run. In such case stops the notifications and activate otuput OnEnd.

        Code: Select all

        virtual void ProcessEvent( EFlowEvent event, SActivationInfo *pActInfo )
        {
            // Every time that one input is modified the node receives an event
            if (event == eFE_Activate)
            {
                // ...
            }

            // Receive this event every frame if updated regularly is active
            if (event == eFE_Update)
            {
                int64 current_time = gEnv->pTimer->GetFrameStartTime().GetMilliSecondsAsInt64();

                if (current_time > end_time)
                {
                    // Limit time reached, stop emitting ticks
                    pActInfo->pGraph->SetRegularlyUpdated(pActInfo->myID, false);
                    ActivateOutput(pActInfo, OUT_ONEND, true);
                    return;
                }

                if (next_tick_time <= current_time)
                {
                    // Set the tick
                    ActivateOutput(pActInfo, OUT_TICK, true);
                    // Set when the next tick must be emitted
                    next_tick_time += interval_time;
                }
            }
        }

        And that's all that we need to make and manage nodes that are updated every frame giving us the way to manage mechanics, players, or any other thing that must be managed every frame and not only when the flowgraph node is activated.

        Check the full tutorial in PDF and whole source code provided in the attached file to see all the parts explained working together.

        For CryEngine V:
        Create your file at <YourProject>\Code\Game\GameDll\flownodes
        For CryEngine 3.8.6 and earlier:
        Create your file at <root>\Code\GameSDK\GameDll\Nodes

        In both cases, you must add your file to your solution to be included in your generated dll.

        Downloadable code from this tutorial

        - ochounos

        Console variables and commands

        #4
        Console access

        The console is not only the place where the log messages are shown but also in a user interface where you can execute console commands and access console variables. Usually in code console variables you will see something named CVar refered to as cvars in documentation.

        To create a console variable, you must use the IConsole interface. To access to the console instance at any time you can use any of these lines:

        Code: Select all

        IConsole* pConsole = gEnv->pConsole;
        IConsole* pConsole = gEnv->pSystem->GetIConsole();

        For the rest of the document we will use a pConsole variable, no matter which method you used to retrieve it.

        Console variables

        Console variables must be registered into the system via IConsole::Register(). You can register four types of variables: int, int64, float, or string:

        Code: Select all

        pConsole->RegisterInt();
        pConsole->RegisterInt64();
        pConsole->RegisterFloat();
        pConsole->RegisterString();

        Let's register our first console variable:

        Code: Select all

        pConsole->RegisterInt("myvar", 88, VF_DEV_ONLY, "This is my custom cvar");

        The parameters are the name of the variable (myvar), the value (88), some flags (VF_DEV_ONLY) and a help text (This is my custom cvar).

        As for the flags, they define how our console variable will be managed. The flag VF_DEV_ONLY means that our cvar will be available only in development mode (not release compilation). Other examples are VF_INVISIBLE, making the cvar only defined in C++ but not exposed into the console, or VF_BLOCKFRAME which blocks the execution of the console for one frame.

        Check IConsole::EVarFlags to find out all possible values available for the console variables flags.

        To get access to our variable we can use the next code:

        Code: Select all

        ICVar* value = pConsole->GetCVar("myvar");
        value->GetIVal();

        Finally, to unregister our variable we use:

        Code: Select all

        pConsole->UnregisterVariable("myvar");

        For your convenience, there are some macros defined in the ISystem.h file that you could use to register console variables:

        Code: Select all

        REGISTER_CVAR(_var,_def_val,_flags,_comment)
        REGISTER_INT(_name,_def_val,_flags,_comment)
        REGISTER_INT64(_name,_def_val,_flags,_comment)
        EGISTER_FLOAT(_name,_def_val,_flags,_comment)
        REGISTER_STRING(_name,_def_val,_flags,_comment)

        Check many other macros that could be useful for you, like creating console vars with callbacks.

        Console commands

        You can define also console conmmands. Console commands are functions defined in C++ following the ConsoleCommandFunc type.

        Code: Select all

        static void RequestParam(IConsoleCmdArgs* pCmdArgs)
        {
           if (pCmdArgs->GetArgCount() == 2)
           {
              const char* param = pCmdArgs->GetArg(1);
              // ...
           }
           else
           {
              CryLog("Error, correct syntax is: mycommand param");
           }
        }

        Now we need to add our console command:

        Code: Select all

        pConsole->AddCommand("mycommand", RequestParam);

        When the command is not needed anymore we can remove it using:

        Code: Select all

        pConsole->RemoveCommand("mycommand");


        Tips
          - You can define the values of your console vars in .cfg files like editor.cfg, system.cgf, project.cfg, or game.cfg, and those values will be applied directly to the console variables when you run the editor, the game, or always if use system.cfg.

          - You have seen the notation name of defined cvars using some letter as first letter, for example r_ , d_ e_ meaning r_ = render, e_ = entity, d_ = debug, ... You can also use these when you define the name of your console variable to note where this variable will be used.

          - You can list all the defined variables in the console using the command DumpCommandsVars. You can also use filter to list only some of the using DumpCommandsVars e_ to list only variables staring with e_ .

          - If you are still using GameSDK you must whitelist your variables. Add your variables to the function CCVarsWhiteList::IsWhiteListed that is located in <root>/Code/GameSDK/GameDll/GameStartup.cpp.

        - ochounos

        Custom Entity in C++

        #5
        Note: This tutorial is based on blank C++ project code provided in CE 5.0 or GameZero code in others versions. To obtain the initial code you should create a new blank C++ project in the launcher. Our custom entity will be created in the folder Code\Game\GameDll\entities.


        Create a new entity

        To create a new entity we should use the template CGameObjectExtensionHelper to simplify the creation of a new GameObjectExtension. Common functionality of entities is created using this template. You should declare your class with the next code:

        Code: Select all

        class CMyEntity : public CGameObjectExtensionHelper<CMyEntity, ISimpleExtension>
        {
            // ...
        };

        The first parameter of the template should be the class to use, in that case our class, and the second one the class to extend. We use ISimpleExtension class. This class extends IGameObjectExtension with a bit more functionality. Check all the methods available in ISimpleExtension.h to know more about what you can do with your entity.


        Useful methods

        Now, we will review some important methods for our new entity that should be implemented:

          - CMyEntity::Init() : Set the members values, allocate resources and return value if initialization was successful.
          - CMyEntity::PostInit() : Initialize the slots, geometries, and other tasks to initialize your entity.
          - CMyEntity::Release() : Free any allocated resource and release the object instance.
          - CMyEntity::ProcessEvent() : Allow to take actions of all the events that the entity could manage.
          - CMyEntity::RegisterProperties() : Allow to create entity properties that will be exposed into the editor and editor properties as icon to show.

        Flowgraph node

        We can also define (though it's optional), a flowgraph node to access to this entity. First, in your header file must define the ports, i.e :

        Code: Select all

        enum EFlowgraphInputPorts
        {
            eInputPorts_LoadGeometry,
        };

        enum EFlowgraphOutputPorts
        {
            eOutputPorts_Done,
        };

        Later, in our code file we must define how to process events related to this flowgraph node. You can do that in the method CMyEntity::OnFlowgraphActivation. Put the logic to manage your node there.

        More info about how to define flowgraph nodes, inputs, and outputs, or to manage events and activate outputs can be found in the next two previous tutorials:

        Custom flowgraph node in C++ Part 1
        Custom flowgraph node in C++ Part 2


        Entity registration

        Finally, before we can use our custom entity in the game, we must register it into the system by adding our new class into CGameFactory::Init() method located in Code\Game\GameFactory.cpp.

        Code: Select all

        RegisterNoScriptGameObject<CMyEntity>("GameMyEntity", "Game/MyEntity", eGORF_None);

        In the above template we pass our C++ class to register it and also the name of our entity, the path to where to find the entity in the editor, and some flags on how to create this entity.
        The allowed flags are:

          - eGORF_None : to not apply any flag.
          - eGORF_HIddenInEditor: in case our entity is not exposed to be used in the editor.
          - eGORF_NoEntityClass : to create the extension but not class.
          - eGORF_InstanceProperties : to create an instance properties table.

        Sample code

        A full implementation of a custom entity is performed in GeomEntity code under the blank C++ project. Just for reference, both files (GeomEntity.h and GeomEntity.cpp) that can be found on Code\Game\GameDll\entities are attached to this post.

        - ochounos
        Attachments
        GeomEntity.zip
        (1.66 KiB) Downloaded 29 times

        Flash integration on C++

        #6
        You can access variables and functions in flash from C++ and also call C++ function from flash. Cryengine provides an interface to manage all those operations easily.


        Interface access

        The interface should be included via “IFlashUI.h “ and can be accessed using the global gEnv->pFlashUI:

        Code: Select all

        #include <CrySystem/Scaleform/IFlashUI.h>
        // ...

        if (gEnv->pFlashUI)
        {
            IUIElement* pHUD = gEnv->pFlashUI->GetUIElement("YourHUDElement");
            if (pHUD)
            {
                // ...
            }
        }

        Note that you must use the name of your element and not the name of your file.
        Check always if the interface has been loaded and also check if you element has been loaded before you use it.


        XML definition

        All the assets that you want to use in C++ must be properly defined in your xml file. You can define variables, arrays, functions, events and movieclips.
        Have a look at the following xml example to see how variables, arrays, functions, events and movie clips are defined inside of our UI Element:

        Code: Select all

        <UIElements name="HudElements">
          <UIElement name="YourHUDElement">
            <GFx file="HUD.gfx" layer="1">
              <Constraints>
                <Align mode="fullscreen" />
              </Constraints>
            </GFx>
         
             <variables>
              <variable name="MyTextField" varname="_root.myTextfield.text"/>
            </variables>
         
            <arrays>
              <array name="MyArray" varname="_root.myarray"/>
            </arrays>

            <functions>
              <function name="SetMessage" funcname="setMessage">
                <param name="Message" desc="Message to show"/>
              </function>
            </functions>
         
            <events>
              <event name="OnClickButton1" fscommand="onMyButtonPressed">
                <param name="Arg1" desc="First argument"/>
                <param name="Arg2" desc="Second argument"/>
              </event>
            </events>
         
            <movieclips>
             <movieclip name="MyMovieClip" instancename="_root.MyMovieClip"/>
            </movieclips>
         
          </UIElement>
         </UIElements>


        Variables and functions access

        When assigning values to variables be sure that the types match. When assigning a value to a variable, we must check if the value was retrieved to ensure that the target variable does not hold an undefined value.

        Code: Select all

        IUIElement* pHUD;

        // Variable access
        pHUD->SetVar("SomeVariable", 12);
        pHUD->SetVar("MyTextField", string("Some text"));

        // Check later if text has expected type
        TUIData text;
        pHUD->GetVariable("MyTextField", text);

        To set an array of variables, we must use the SUIArgument struct as shown:

        Code: Select all

        // set array
        SUIArguments myArray;
        myArray.AddArgument(12);
        myArray.AddArgument(string("mystring"));
        myArray.AddArgument(12.34f);
        pHUD->SetArray("MyArray", myArray);

        // get array
        SUIArguments myArray;
        pHUD->GetArray("MyArray", myArray);

        To call functions we must pass the arguments using also the SUIArgument struct:

        Code: Select all

        SUIArguments args;
        args.AddArgument(12);
        args.AddArgument(string("mystring"));

        TUIData res;

        pHUD->CallFunction("SetMessage", args, &res);


        Movie clips

        You can access movieclips from flash, for better management of play sequences, directly. A movieclip can be created or modified using the IFlashVariableObject:

        Code: Select all

        IFlashVariableObject* pMC = pHUD->GetMovieClip("MovieClip1");
        if (pMC)
        {
           // ...
        }


        Events

        We can call functions on C++ from flash using events. In C++ we must define an IUIElementEventListener and register in our IUIElement:

        Code: Select all

        class CHUDEventListener : public IUIElementEventListener
        {
        public:
           virtual void OnUIEvent(const SUIEventDesc& event, const SUIArguments& args)
           {
              if (std::strcmp(event.sDisplayName, "OnButton1") == 0)
              {
                 int arg1;
                 int arg2;
                 args.GetArg(0, arg1);
                 args.GetArg(1, arg2);
                 // ...
              }
              // ...
           }
        };

        // ...

        // Registering the listener
        CHUDEventListener eventListener;

        if (gEnv->pFlashUI)
        {
           IUIElement* pHUD = gEnv->pFlashUI->GetUIElement("MyHUDElement");
           if (pHUD)
           {
              pHUD->AddEventListener(&eventListener);

           }
        }

        The OnUIEvent will manage events defined in your xml. If you have many of them or if you are using variables depending on some condition, you could use OnUIEventEx function that will manage any other fscommand calls not defined in your xml.
        Other events that you can manage in the listener for this UIElement are OnInit, OnSetVisible, OnInstanceCreated and OnInstanceDestroyed.

        To activate events from flash you should call them via fscommand:

        Code: Select all

        fscommand("OnButton1"); // Case sensitive

        Note that the last line of code is action script 2 flash code and not C++ code, you should use it in your as2 file or flash code.

        - ochounos

        Action maps in C++

        #7
        Action maps

        To manage the controls of your game, CRYENGINE uses predefined actions that can be grouped into so called action maps. You can define action maps for certain situations in your game e.g. first person player controls, vehicles, cutscenes, menus etc.
        You can enable or disable action maps during runtime via flowgraph, lua scripts or C++ code.

        By using CryAction, the predefined action map “default” is activated by default and listens for inputs that should be processed next to player related actions (e.g. ui commands).

        You can visualize the status of loaded action maps and filters with the cvar “i_listactionmaps” (1 for all loaded maps, 2 for active ones only).

        By using the actionmapmanager default entity (can be set through code and flowgraph), the input gets forwarded to the specified entity input extension automatically. It makes things easier for you, when there is only one entity (e.g. player) that should handle input in your game.

        This is the default profile included in the blank C++ project under the file Assets\Libs\config\defaultprofile.xml:

        Code: Select all

        <profile version="0">
           <platforms>
              <PC keyboard="1" xboxpad="1" ps4pad="1"/>
              <XboxOne keyboard="1" xboxpad="1" ps4pad="0"/>
              <PS4 keyboard="1" xboxpad="0" ps4pad="1"/>
           </platforms>
           
           <!--DO NOT REMOVE DEFAULT ACTIONMAP! NEEDED BY CRYACTION.-->
           <actionmap name="default" version="1"/>
           <!---->
           
           <actionmap name="player" version="1">
              <action name="moveleft" onPress="1" onRelease="1" retriggerable="1" keyboard="a"/>
              <action name="moveright" onPress="1" onRelease="1" retriggerable="1" keyboard="d"/>
              <action name="moveforward" onPress="1" onRelease="1" retriggerable="1" keyboard="w"/>
              <action name="moveback" onPress="1" onRelease="1" retriggerable="1" keyboard="s"/>
              <action name="moveup" onPress="1" onRelease="1" retriggerable="0" keyboard="space" xboxpad="xi_a" ps4pad="pad_cross"/>
              <action name="movedown" onPress="1" onRelease="1" retriggerable="0" keyboard="lctrl" xboxpad="xi_x" ps4pad="pad_square"/>
              <action name="boost" onPress="1" onRelease="1" keyboard="lshift" xboxpad="xi_thumbl" ps4pad="pad_l3"/>
              
              <action name="mouse_rotateyaw" keyboard="maxis_x" />
              <action name="mouse_rotatepitch" keyboard="maxis_y" />
              
              <action name="controller_movey" xboxpad="xi_thumbly" ps4pad="pad_stickly"/>
              <action name="controller_movex" xboxpad="xi_thumblx" ps4pad="pad_sticklx"/>
              <action name="controller_rotateyaw" xboxpad="xi_thumbrx" ps4pad="pad_stickrx"/>
              <action name="controller_rotatepitch" xboxpad="xi_thumbry" ps4pad="pad_stickry"/>
           </actionmap> 
        </profile>

        Every action has the name to use internally in C++ to identify this action, some activation modes, some modifiers and key associated (plus some xbox or ps4pad controller if defined).

        The modifiers per key that you can use are:
          - onPress: the action key is pressed.
          - onRelease: the action key is released.
          - onHold: the action key is held.
          - always: permanently.
        You can define also other modifiers:
          - retriggerable: the action can be triggered repeatedly.
          - holdTriggerDelay: allow some delay before the onHold is triggered.
          - holdRepeatDelay: if the dealy should be repeated after first delay.
          - noModifiers: action only takes place if no control, shift, alt, or win keys are pressed.
          - consoleCmd:- action corresponds to a console command.
          - pressDelayPriority: priority of this press action.
          - pressTriggerDelay: delay until pess is triggered.
          - pressTriggerDelayRepeatOverride: if the repeat delay could be override by others.
          - inputsToBlock: specify the input actions to block here.
          - inputBlockTime: time to block the specified input action.
        In your action maps you can define also filters, allowing or disabling some actions:

        Code: Select all

        <actionfilter name="no_move_side" type="actionFail">
                <action name="moveleft"/>
                <action name="moveright"/>
        </actionfilter>

        The type of the filter can be actionFail or actionPass, disabling or allowing respectively actions to trigger events.

        Read more about Action map manager in the next link Action map manager


        Input extension

        We can create game object extensions not only to create new custom entities but also to manage some aspects of the game.
        (To know more about how to create custom entities please read the tutorial "Custom entities in C++")

        We will create a game object extension that will manage our input controlles (keyboard, xbox or ps4 pad).

        Code: Select all

        class CInputExtension : public CGameObjectExtensionHelper<CInputExtension, ISimpleExtension>, IActionListener, ISystemEventListener
        {
           CInputExtension();
           virtual ~CInputExtension();
           virtual void PostInit(IGameObject* pGameObject) override;

           // ...

        private:
           bool OnActionMoveLeft(EntityId entityId, const ActionId& actionId, int activationMode, float value);
           bool OnActionMoveRight(EntityId entityId, const ActionId& actionId, int activationMode, float value);

           // ...

           void RegisterInputHandler();

           TActionHandler<CInputExtension> m_playerActionHandler;
           Vec3 m_vDeltaMovement;

           // ...
        };

        We must add to our handler all the actions defined in our action map:

        Code: Select all

        void CInputExtension::RegisterInputHandler()
        {
           m_playerActionHandler.AddHandler(ActionId("moveleft"),   &CInputExtension::OnActionMoveLeft);
           m_playerActionHandler.AddHandler(ActionId("moveright"),   &CInputExtension::OnActionMoveRight);
           // ...
        }

        Then we must init the action map capturing events when our input extension is initiated:

        Code: Select all

        void CInputExtension::PostInit(IGameObject* pGameObject)
        {
           RegisterInputHandler();

           IActionMapManager* pActionMapManager = gEnv->pGame->GetIGameFramework()->GetIActionMapManager();
           if (pActionMapManager)
           {
              pActionMapManager->InitActionMaps("libs/config/defaultprofile.xml");
              pActionMapManager->SetDefaultActionEntity(GetEntityId());
              pActionMapManager->EnableActionMap("player", true);
              pActionMapManager->Enable(true);
           }

           pGameObject->CaptureActions(this);
           // ...
        }

        In the code above, the GetEntityId() points to the parent entity that have attached this input extension. We will define later this association. You can also identify there the file to be loaded with our action maps and the action map ("player") to be used. use the same method to load at any time other action map files and enable or disable maps.

        Finally, we just need to define what to do when the action is triggered:

        Code: Select all

        bool CInputExtension::OnActionMoveLeft(EntityId entityId, const ActionId& actionId, int activationMode, float value)
        {
           if (activationMode == eAAM_OnPress)
              m_vDeltaMovement.x = -value;
           else if (activationMode == eAAM_OnRelease && m_vDeltaMovement.x < 0.0f)
              m_vDeltaMovement.x = 0.0f;

           return false;
        }

        bool CInputExtension::OnActionMoveRight(EntityId entityId, const ActionId& actionId, int activationMode, float value)
        {
           if (activationMode == eAAM_OnPress)
              m_vDeltaMovement.x = value;
           else if (activationMode == eAAM_OnRelease && m_vDeltaMovement.x > 0.0f)
              m_vDeltaMovement.x = 0.0f;

           return false;
        }

        Once we have our input extension defined, we can review how this extension is attached to another game object extension (or custom entity) to interact with this input extension. Lets see an example with a CPlayer entity:

        Code: Select all

        void CPlayer::PostInit(IGameObject* pGameObject)
        {
           pGameObject->AcquireExtension("InputExtension");
        }

        This extension name defined in the string constant, must match an entity already defined and registered in the game. To register our input extension we must do it in the game factory:

        Code: Select all

        void CGameFactory::Init()
        {
           // ...
           RegisterGameObject<CInputExtension>("InputExtension", "", eGORF_NoEntityClass);
           // ...
        }

        Check the "Custom entities in C++" to know more on how to register entities in the game.

        Next tutorial we will see how to create movement extension and view extension to manage the movement of a player and the camera of the game using the info provided by the input controllers.

        You can review how input extension is fully defined in Code\Game\GameDll\player\extensions\InputExtension.h and InputExtension.cpp. Check also Code\Game\GameDll\player\Player.cpp and Code\Game\GameDll\game\GameFactory.cpp.

        For your convenience and just in case that you don't have access to blank C++ code when reading this tutorial the involved files (InputExtension.h, InputExtension.cpp, Player.h, Player.cpp, GameFactory.h, GameFactory.cpp and defaultprofile.xml) are included attached to this post.

        - ochounos
        Attachments
        ActionMaps.zip
        (5.38 KiB) Downloaded 24 times

        Game Object Extensions

        #8
        Game Object Extensions

        In the last tutorial we worked out how to add an input extension to a game object. If you didn't read the last tutorial Action Maps in C++ I recommend to start there to get a better understanding of game object extensions and their purpose in CryENGINE.
        The last tutorial added an input extension to manage input coming from the registered input devices, this tutorial will use values from those inputs to relocate the entity position and camera view of the scene. We will add such extensions to our player allowing to manipulate the movement and camera of the player.


        Movement extension

        First we define a “movement” extension using the CGameObjectExtensionHelper:

        Code: Select all

        class CMovementExtension : public CGameObjectExtensionHelper<CMovementExtension, ISimpleExtension>
        {
        public:
           // ISimpleExtension
           virtual void PostInit(IGameObject* pGameObject) override;
           virtual void PostUpdate(float frameTime) override;
           virtual void Release() override;
           // ~ISimpleExtension
           
           CMovementExtension();
           virtual ~CMovementExtension();

           //...
        };

        We should also tell the GameObject associated that we want to receive post updates:

        Code: Select all

        void CMovementExtension::PostInit(IGameObject* pGameObject)
        {
           pGameObject->EnablePostUpdates(this);
        }

        After that we will receive notifications every frame. In this PostUpdate method we can manage the movement depending on the controllers, so we must retrieve info from input extension and then move our entity to some position in the map depending on what keys the user pressed.

        Code: Select all

        void CMovementExtension::PostUpdate(float frameTime)
        {
           SmartScriptTable inputParams(gEnv->pScriptSystem);
           if (GetGameObject()->GetExtensionParams("InputExtension", inputParams))
           {
              Ang3 rotation(ZERO);
              inputParams->GetValue("rotation", rotation);
           
              Quat viewRotation(rotation);      
              GetEntity()->SetRotation(viewRotation.GetNormalized());

              Vec3 vDeltaMovement(ZERO);
              inputParams->GetValue("deltaMovement", vDeltaMovement);

              GetEntity()->SetPos(GetEntity()->GetWorldPos() + viewRotation * (vDeltaMovement * frameTime * m_movementSpeed));
           }
        }

        From the code above we can note some actions performed:

          - To retrieve values from an extension we use implemented scriptTables that we can access through GetGameObject()->GetExtensionParams(“ExtensionName”, inputParams).

          - Since script tables are nested layers of properties we can now access the actual movement variable of type Vec3 with a similiar command: inputParams->GetValue(“PropertyName”, rotation);

          - Only thing left to do is to calculate the new position/rotation and apply it to our game object:
            GetEntity()->SetRotation(new value);
            GetEntity()->SetPos(new value);

        You can play around with those methods to displace the entity depending on the input values. A good practice task could be to try and add a key for jumping or add some acceleration to an axis to simulate low gravity.

        Keep in mind that this parameter manipulation is just relocating the entity and is not in charge of collisions with terrain or other entities. Proper physics interaction will be covered in a future tutorial.


        View extension

        Modifiyng the base position of an entity is pretty easy – but we still have to take care of moving the actual view of the main scene camera to see an effect like it is showcased in game zero. As usual we must define our extension using CGameObjectExtensionHelper but this time we will include another interface called “IGameObjectView”.

        Code: Select all

        class CViewExtension : public CGameObjectExtensionHelper<CViewExtension, ISimpleExtension>, IGameObjectView
        {
        public:
           // ISimpleExtension
           virtual void PostInit(IGameObject* pGameObject) override;
           virtual void Release() override;
           // ~ISimpleExtension

           // IGameObjectView
           virtual void UpdateView(SViewParams& params) override;
           virtual void PostUpdateView(SViewParams& viewParams) override {}
           // ~IGameObjectView

           CViewExtension();
           virtual ~CViewExtension();

        private:
           void CreateView();

           unsigned int m_viewId;
           float m_camFOV;
        };

        void CViewExtension::PostInit(IGameObject* pGameObject)
        {
           CreateView();
           pGameObject->CaptureView(this);
        }

        void CViewExtension::CreateView()
        {
           IViewSystem* pViewSystem = gEnv->pGame->GetIGameFramework()->GetIViewSystem();
           IView* pView = pViewSystem->CreateView();

           pView->LinkTo(GetGameObject());
           pViewSystem->SetActiveView(pView);

           m_viewId = pViewSystem->GetViewId(pView);
        }

        We just created a new method “CreateView” to create a new IView object and associate (link) it to our game object. Then we tell the ViewSystem about our newly created IView object and set it as the currently active view. We also store the viewId in a member variable to be able to access it later on.
        As long as our game object is linked to the view, the ViewSystem will notify us about updates in the UpdateView() method. Here we can manipulate the currently used SViewParams by copying over the values from our game object (the values we just set through our movement extension):

        Code: Select all

        void CViewExtension::UpdateView(SViewParams& params)
        {
           params.position = GetEntity()->GetWorldPos();
           params.rotation = GetEntity()->GetWorldRotation();
           params.fov = DEG2RAD(m_camFOV);
        }

        Note: If you don't change the default camera position, you will notice that your camera is inside of the player so you should move it out, i.e. two meters over the player and then rotate a bit to see the player in front of you. Now you can try to define a top-down camera (positioning z-axis and rotate 90 degrees on x-axis) or create something cool like a third person over-shoulder camera for the player.


        Register extensions

        Last but not least we have to tell our game object about the extensions we want to use. In our case we want this to happen as soon as possible after game init, so we’re using the CPlayer::PostInit() method:

        Code: Select all

        void CPlayer::PostInit(IGameObject* pGameObject)
        {
           pGameObject->AcquireExtension("ViewExtension");
           pGameObject->AcquireExtension("InputExtension");
           pGameObject->AcquireExtension("MovementExtension");
        }

        When your player is released, you shoud release the extensions again:

        Code: Select all

        void CPlayer::Release()
        {
           GetGameObject()->ReleaseExtension("ViewExtension");
           GetGameObject()->ReleaseExtension("InputExtension");
           GetGameObject()->ReleaseExtension("MovementExtension");

           ISimpleExtension::Release();
        }

        Don't forget to register your extensions in the GameFactory (just like any other entity in the game):

        Code: Select all

        void CGameFactory::Init()
        {
           //...
           RegisterGameObject<CInputExtension>("InputExtension", "", eGORF_NoEntityClass);
           RegisterGameObject<CMovementExtension>("MovementExtension", "", eGORF_NoEntityClass);
           RegisterGameObject<CViewExtension>("ViewExtension", "", eGORF_NoEntityClass);
           //...
        }

        With those three extensions you should have everything you need to get started with your very own player implementation.

        All the code (with little modifications for better understanding) is already available in a blank C++ template project coming with CryENGINE.

        - ochounos

        Who is online

        Users browsing this forum: No registered users and 2 guests