Thread: From script to C to C++

  1. #1
    Registered User VirtualAce's Avatar
    Join Date
    Aug 2001
    Posts
    9,607

    From script to C to C++

    Shakti and myself have come up with what we think is a powerful system for doing scripting. A lot of this system was developed by tearing MFC's message system apart piece by excruciating piece. The final product is something I think is invaluable to game development for many of us here.

    First we have decided the script is not going to be constantly sent to the game objects during game time. A script in itself defines what every object is going to do and when, so why keep sending stuff that either has been done or is going to be done. So basically everyone is given their script and then they execute it as it is layed out. So each object is jumping through it's own script at run time and the script class doesn't even exist anymore. After all, the script will never change once it has been written.

    The second major problem we had to address was going from text files to actual C functions and C++ functions. This problem has been addressed quite nicely in the heart of MFC but it is not pretty code. MFC's example uses hashing to find the message handlers but our method uses an STL container.


    The main header file for our scripts. I didn't say the code was pretty but it works just fine.

    ScriptCmn.h
    Code:
    #pragma once
    
    #include <map>
    //#include "CGameObject.h"
    
    /////////////////////////////////
    //Script message map structures//
    /////////////////////////////////
    
    //Forward delcaration of CGameObject;
    class CGameObject;
    
    //SCRIPT_MSG typedef
    typedef void (CGameObject::*SCRIPT_MSG)(void);
    
    //SCRIPT_MSGMAP_ENTRY
    
    struct SCRIPT_MSGMAP_ENTRY
    {
      UINT nCommandID;
      UINT nFuncSig;
      SCRIPT_MSG pfn;
    
    };
    
    //SCRIPT_MSGMAP
    struct SCRIPT_MSGMAP
    {
      SCRIPT_MSGMAP *pBaseMap;
      std::map<UINT,SCRIPT_MSGMAP_ENTRY *> mapEntries;
    
      SCRIPT_MSGMAP():pBaseMap(NULL) { }
    
      SCRIPT_MSGMAP(SCRIPT_MSGMAP *S_BaseMap,std::map<UINT,SCRIPT_MSGMAP_ENTRY *> S_mapEntries)
      {
         pBaseMap=S_BaseMap;
    
         mapEntries.reserve(S_mapEntries.size());
    
         for (UINT i=0;i<static_cast<UINT>(mapEntries.size());i++)
         {
            mapEntries[i]=S_mapEntries[i];
         }
      }
    
    };
    
    enum SFuncSigs
    {
      SFSig_Param0_n =0,
      SFSig_Param1_n,  //int fxn(DWORD)
      SFSig_Param2_n,  //int fxn(DWORD,DWORD)
      SFSig_Param3_n   //int fxn(DWORD,DWORD,DWORD)
    };
    
    union ScriptMsgMapFuncs
    {
      SCRIPT_MSG pfn;
      UINT (CGameObject::*pfnParam1_n)(DWORD);
      UINT (CGameObject::*pfnParam2_n)(DWORD,DWORD);
      UINT (CGameObject::*pfnParam3_n)(DWORD,DWORD,DWORD);
    };
    
    ///////////////////////////////
    //Message map macros//
    ///////////////////////////////
    
    #define DECLARE_SCRIPT_MSGMAP() \
    private: \
      static const std::map<UINT,SCRIPT_MSGMAP_ENTRY *> m_mapEntries; \
    protected: \
      static const SCRIPT_MSGMAP m_MsgMap; \
      virtual const SCRIPT_MSGMAP *GetMessageMap() const;
    
    #define BEGIN_SCRIPT_MSGMAP(theClass,baseClass) \
    const SCRIPT_MSGMAP *GetMessageMap() const \
    { return &theClass::m_MsgMap; } \
    const SCRIPT_MSGMAP theClass::m_MsgMap = \
    { &baseClass::m_MsgMap, &theClass::m_mapEntries }; \
    theClass::m_MsgMap. 
    
    #define ON_SCRIPT_CMD1(id,memberFxn) \
    Add(id,SCRIPT_MSGMAP_ENTRY(id,SSig_Param0_n,(SCRIPT_MSG)memberFxn); 
    
    #define ON_SCRIPT_CMD1(id,memberFxn) \
    Add(id,SCRIPT_MSGMAP_ENTRY(id,SSig_Param1_n,(SCRIPT_MSG)memberFxn); 
     
    #define ON_SCRIPT_CMD2(id,memberFxn) \
    Add(id,SCRIPT_MSGMAP_ENTRY(id,SSig_Param2_n,(SCRIPT_MSG)memberFxn); 
    
    #define ON_SCRIPT_CMD3(id,memberFxn) \
    Add(id,SCRIPT_MSGMAP_ENTRY(id,SSig_Param3_n,(SCRIPT_MSG)memberFxn); 
    
    
    /////////////////////
    //Script structures//
    /////////////////////
    
    //Function structure filled out by script class
    
    struct SCRIPTFUNC
    {
      UINT nCommandID;  //Script command ID
      UINT nParam1;
      UINT nParam2;
      UINT nParam3;
      CString strDebugID;
    };
      
    
    //One command from script
    struct ScriptCommand
    {
      bool bExecuted;
      SCRIPTFUNC *pFuncInfo;
      ScriptCommand *pNext;
    };
    
    //One if block from script
    struct ScriptIfBlock
    {
      bool bExecuted;
      ScriptCommand *pCommands;
      ScriptIfBlock *pNestedIfs;
      ScriptIfBlock *pNext;
    };
    
    //One complete event from script
    struct ScriptEvent
    {
      bool bExecuted;
      ScriptIfBlock *pBlocks;
      ScriptEvent *pNestedEvents;
      ScriptEvent *pNext;
    };
    
    //One trigger from script
    struct ScriptTrigger
    {
      bool bExecuted;
      ScriptEvent *pEvents;
      ScriptTrigger *pNestedTriggers;
      ScriptTrigger *pNext;
    };
    
    //One quest from script
    struct ScriptQuest
    {
      ScriptTrigger *pTriggers;
      ScriptQuest *pNestedQuests;
      ScriptQuest *pNext;
    };
    
    //One type of attack from script
    //An attack is a list of commands to be executed
    struct AttackScript
    {
      ScriptEvent *pAttack;
      AttackScript *pNestedAttacks;
      AttackScript *pNext;
    };

    The workings of this little beasty are quite complex but the result is that to respond to a message you do this:

    Code:
    class CMyObject::public CGameObject
    {
      DECLARE_SCRIPT_MSGMAP();
      ....
    };
    Code:
    #include "stdafx.h"
    #include "CMyObject.h"
    
    BEGIN_SCRIPT_MSGMAP(CMyObject,CObject)
      ON_SCRIPT_CMD1(ID_MOVETOWPL,MoveToWpList)
    END_SCRIPT_MSGMAP
    
    ...
    ...
    
    UINT CMyObject::MoveToWpList(UINT nID)
    {
      CWaypointList *pList=GetWayPointList(nID);
    
      D3DXVECTOR2 vecPos2;
      pList->GetPos(0,&vecPos2);
    
      D3DXVECTOR vecDiff2=m_vecPos2-vecPos2;
    
      float fLength=D3DXVec2Length(&vecDiff2);
      
      m_vecNextPos2.x=vecDiff2.x/fLength;
      m_vecNextPos2.y=vecDiff2.y/fLength;
    }

    The main MsgProc of CGameObject calls OnScriptMsg if the msgID is indeed an object message. If not, some other processing is done to determine if it's a button, engine command, etc, etc. OnScriptMsg() finds the handler in the std::map when given the ID of the command. If a handler exists, the message is processed. If not, control is passed to the default MsgProc which calls the default OnScriptMsg() to handle the message. If OnScriptMsg() does not have a handler for the message, the message is ignored.

    So far the setup seems to work just fine. Also note that if a handler does not exist, we can easily trap for this and write the information to a file for easy script debugging. The file could read something like this:

    00:00:23 - No handler for object "Human1" at 125,200 for aMoveToWPL().

    We would like comments on the system so we can see all angles because it's easy to miss something along the way. If you can see any major pitfalls, let us know.
    Last edited by VirtualAce; 09-10-2006 at 07:08 PM.

  2. #2
    Registered User
    Join Date
    Aug 2003
    Posts
    1,218
    I have an angle we didnt discuss last nite that has to do with moving the script execution to the object:
    Since the object will keep track of where it is in the script all the time and only change that once its done doing that thing, how are you supposed to allow the object to start doing something else?? Say its walking around a waypoint-plotted way, but you also want the object to start to run once it sees an enemy, or attack, or walk to the object and start talking or anything along those lines??
    STL Util a small headers-only library with various utility functions. Mainly for fun but feedback is welcome.

  3. #3
    Registered User VirtualAce's Avatar
    Join Date
    Aug 2001
    Posts
    9,607
    AI intervention
    Thats where the engine AI code comes in. Being alerted by another object is going to kick in the AI processing that we put into the engine. The script is what will happen if our objects were in a perfect game world. However, we know that character such and such might get hacked and slashed to death or alerted in which case the current script command and processing will be placed on hold. To attack the object, the class will then execute it's attack script in the same manner as the overall script. Note that during these times though, the AI class in the engine can take over the object and make it do other things. It may tell the object to run like hell to a point it specifies or it may tell the object to fight to the death.

    Each object is a small processor
    Our objects are simple processors that process a finite set of instructions and just like our CPU's they can only process/execute one thing at a time. So the script creator can either create all the actions in order, or they can setup variables to ensure the script can be executed in order. The way to get around the problem you are talking about is to keep a list of script message process structures that essentially contain an index to the current command in the conditional block. The object will have to track all conditional blocks in order to work correctly. So for each conditional we have a script message process structure that is essentially the 'stack' for that process. Once that process is done, we can set executed to one and/or simply start the if over again depending on usage.

    Process structures - our equivalent of CS:EIP in assembly
    Code:
    struct SCRIPT_MSG_PROCESS_INFO
    {
      UINT uBlockIndex;    //The current position within the current if block
      UINT uIfIndex;          //The current position inside the if block's (to support nested if's)
      SCRIPT_MSG_PROCESS_INFO *pNext;
    };
    This means that uBlockIndex is always an offset into uIfIndex. So 0,3 would mean we are on the fourth command in the first if block.

    And at load-time we will create a list of these as

    The script process object - used to index into our script list
    Code:
    struct SCRIPT_MSG_PROCESS_MAP
    {
      SCRIPT_MSG_PROCESS_INFO *pProcessInfo;
      //No need for a base map here - one map represents the entire script for this object
    };
    Executing the script is simple iteration of the process object
    So now the main script process inside of CGameObject becomes a simple iteration of the SCRIPT_MSG_PROCESS_MAP. The object simply extracts the relevant information from the data structures we hav already defined and begins processing the messages. This is no different than what you were doing in your script except we have now delegated it to CGameObject and we have a mechanism by which to determine and track the current script if block and current script command. Essentially we have created a CODE SEGMENT or CS and an instruction pointer or CS:EIP just like in assembly language. Just as those 2 beasties track which portion of code is executing at any one time, our system does the same thing.

    Object synchronization
    Also, this way we have the script working as planned but the AI can intervene and mess the whole thing up. Also if the object dies, it's script portion dies with it and hence less processing. With our other approach the object would never die and if it did, it would still attempt to send a message to the dead object. In this new approach the object is completely removed from the picture and no one cares. Synchronization for objects comes from the script and due to the fact that every object's movements and actions will be based on the current frameDelta. So if everyone moves according to this delta, regardless of their speed, the game will be in sync and all objects will be in sync. This requires one more parameter to add support for the delta. Instead of calling some huge update function which in reality does not know how to update the objects, the ScriptMsgProc will update the objects. So without the script all we have is a really pretty still life scene with nothing going on - which is how it should be. The script brings everything to life and gives total control of the system to us....the creators.

    BTW that code compiles just fine and it works as planned which is amazing given the thousands of lines of MFC source we looked at. I hope you like what I came up with for a header file for the script system. Hopefully we can do TS again and talk this over. Did a lot more coding last night and the animation editor is nearly done.
    Last edited by VirtualAce; 09-11-2006 at 04:57 AM.

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Script in games
    By Shakti in forum Game Programming
    Replies: 7
    Last Post: 09-27-2006, 12:27 AM
  2. In a game Engine...
    By Shamino in forum Game Programming
    Replies: 28
    Last Post: 02-19-2006, 11:30 AM
  3. how to implementate a registration script
    By TJa in forum C++ Programming
    Replies: 0
    Last Post: 10-28-2005, 02:33 AM
  4. Passing arguments to script.....
    By suwie in forum C Programming
    Replies: 5
    Last Post: 09-25-2004, 11:10 PM
  5. Game structure, any thoughts?
    By Vorok in forum Game Programming
    Replies: 2
    Last Post: 06-07-2003, 01:47 PM