Game Development Community

dev|Pro Game Development Curriculum

TGEA 1.7.1 Custom Hardware Cursors

by Dave Young · 09/24/2008 (6:49 am) · 12 comments

I make use of context sensitive cursor swapping in RPG games, and the new TGEA makes it a little trickier due to its use of hardware/OS cursor. The cursor performance is very smooth with OS cursors and its worthwhile, so with the ability to swap cursors at runtime the combination is a winning one. I made these changes in the ArcaneFX codebase which is based off 1.7.1.

I have tried this with both animated cursors (.ani) and static cursors (.cur), both of which were colored.

This is a little hackish and probably has several areas for improvement, but it should be an excellent start.

It works by exposing to the console two new commands.

Canvas.addPlatformCursor(%cursorfile,%id);

Example:

Canvas.addPlatformCursor("mycursor.ani",111);
This loads up the ani file as a new cursor type, and sets its ID as 111. The ID is arbitrary, but I'm using ones that are not already used in the preloaded cursor enums. Of course, you need to add the new cursor before you use it :)

Canvas.setPlatformCursor(111);
Sets the current cursor to the one loaded previously. This does support multiple custom cursors.

Now for engine changes:

in gui/core/guiCanvas.h:
under:
virtual void setCursor(GuiCursor *cursor);

add:
virtual void setPlatformCursor(S32 cursor);
   virtual bool addPlatformCursor(const char* cursorName, U32 cursorID);

in gui/core/guiCanvas.cpp:
void GuiCanvas::setPlatformCursor(S32 cursorID)
{
   GuiCanvas *pRoot = this;
   if( !pRoot )
      return;

   PlatformWindow *pWindow = pRoot->getPlatformWindow();
   AssertFatal(pWindow != NULL,"GuiControl without owning platform window!  This should not be possible.");
   PlatformCursorController *pController = pWindow->getCursorController();
   AssertFatal(pController != NULL,"PlatformWindow without an owned CursorController!");

   S32 desiredCursor;
   switch(cursorID)
   {
      case 0:
	     desiredCursor = PlatformCursorController::curArrow;
		 //Con::errorf("Making it an arrow");
	     break;
      case 1:
	     desiredCursor = PlatformCursorController::curPlus;
		 //Con::errorf("Making it a crosshair");
		 break;
	  default:
		desiredCursor = cursorID;
		//Con::errorf("Making it a CUSTOM cursor");
		break;
   }

   // Bail if we're already at the desired cursor
   if(pRoot->mCursorChanged == desiredCursor )
   {
	  //Con::errorf("Already at desired shape");
      return;
   }

   // Now change the cursor shape
   if(pRoot->mCursorChanged != -1)
         pController->popCursor();
   pController->pushCursor(desiredCursor);
   pRoot->mCursorChanged = desiredCursor;
}

ConsoleMethod( GuiCanvas, setPlatformCursor, void, 3, 3, "(cursorName)")
{
   S32 curs = dAtoi(argv[2]);
   object->setPlatformCursor(curs);
}

bool GuiCanvas::addPlatformCursor(const char* cursorName, U32 cursorID)
{
   GuiCanvas *pRoot = this;
   if( !pRoot )
   {
      //Con::errorf("No root, returning");
      return false;
   }
   PlatformWindow *pWindow = pRoot->getPlatformWindow();
   AssertFatal(pWindow != NULL,"GuiControl without owning platform window!  This should not be possible.");
   PlatformCursorController *pController = pWindow->getCursorController();
   AssertFatal(pController != NULL,"PlatformWindow without an owned CursorController!");

   bool retval;
   retval = pController->addCustomPlatformCursor(cursorName, cursorID);
   return retval;
}

ConsoleMethod( GuiCanvas, addPlatformCursor, bool, 4, 4, "addPlatformCursor(cursorName,cursorID)")
{
   return object->addPlatformCursor(argv[2],dAtoi(argv[3]));
}

The setPlatformCursor function also lets me set to the system plus/crosshair cursor, I added that in and I will probably add several more uses of pre-existing cursors to that function. A call to setPlatformCursor(0) gives me the default arrow, and if I give it a number it can't find, it assumes it's a custom cursor and continues without failing.


Next, in windowManager\PlatformCursorController.h, add the bolded line:
virtual S32 getDoubleClickHeight() = 0;
   [b]virtual bool addCustomPlatformCursor(const char* cursorName, U32 cursorID);[/b]


Next, in windowManager\PlatformCursorController.cpp

add:
bool PlatformCursorController::addCustomPlatformCursor(const char* cursorName, U32 cursorID)
{
	return addCustomPlatformCursor(cursorName, cursorID);
}


Next, in windowManager\win32\win32CursorController.h, add the following lines directly underneath the #include section

#include "windowManager/platformCursorController.h"

class customPlatformCursor
{
   public:
      HCURSOR customCursorHandle;
      U32 cursorID;
};

Next, in windowManager\win32\win32CursorController.h, add the bolded lines
S32 getDoubleClickHeight();
   
   [b]Vector<customPlatformCursor*> customPlatformCursors;
   bool addCustomPlatformCursor(const char* cursorName, U32 cursorID);[/b]



Next, in windowManager\win32\win32CursorController.cpp:

add this to the #includes at the top:

#include "core/unicode.h"

Change your existing setCursorShape to this:
void Win32CursorController::setCursorShape(U32 cursorID)
{
   LPTSTR resourceID = NULL;

   for(S32 i = 0;sgCursorShapeMap[i].resourceID != NULL;++i)
   {
      if(cursorID == sgCursorShapeMap[i].id)
      {
         resourceID = sgCursorShapeMap[i].resourceID;
         break;
      }
   }

   if(resourceID == NULL)
   {
      for(S32 i = 0;i < customPlatformCursors.size();++i)
      {
		  //Con::errorf("Looking at %d %d",i,customPlatformCursors[i]->cursorID);
         if(cursorID == customPlatformCursors[i]->cursorID)
         {
			//Con::errorf("Found existing custom cursor, setting %d",cursorID);
			SetCursor(customPlatformCursors[i]->customCursorHandle);
            return;
         }
      }
   }

   if(resourceID == NULL)
   {
      //Con::errorf("No resource found for cursor id %d",cursorID);
   } else 
   {

	HCURSOR cur = LoadCursor(NULL, resourceID);
	if(cur)
	{
		//Con::errorf("Setting cursor %d",cursorID);
		//mLastCursorID = cursorID;
		SetCursor(cur);
	}
	else
	{
		//Con::errorf("No cursor found for %d",cursorID);
    }
   }
}

bool Win32CursorController::addCustomPlatformCursor(const char* cursorName, U32 cursorID)
{
	UTF16 cursorTitle[256] = L"";

   #ifdef UNICODE
      convertUTF8toUTF16((UTF8 *)cursorName, cursorTitle, sizeof(cursorTitle));
   #else
      dStrcpy(cursorName, cursorTitle);
   #endif

  //Look for existing ID first
  for(S32 i = 0;i < customPlatformCursors.size();++i)
  {
     if(cursorID == customPlatformCursors[i]->cursorID)
     {
        return false;
      }
   }

   HCURSOR cur = LoadCursorFromFile(cursorTitle);
   if(cur)
   {
      //Con::errorf("loaded custom platform cursor %s with id %d",cursorName,cursorID);
      customPlatformCursor *newCur = new customPlatformCursor();
      newCur->customCursorHandle = cur;
      newCur->cursorID = cursorID;
      customPlatformCursors.push_back(newCur);
      return true;
   }
   
   //Con::errorf("Failed to load platform cursor: %s %d",cursorName,cursorID);
   return false;

}

Note: if you want to uncomment the Con::errorf statements to debug, you will need to add:
#include "console/console.h"

at the top of the .cpp in which you want to use the Con:: namespace

There was one last change I needed to make in order to get this running well, and that was to stop guiControl from resetting (popCursor()) the cursor if the cursorID was > 100

in gui/core/guiControl.cpp, my getCursor function looks like this (note the bolded lines)
void GuiControl::getCursor(GuiCursor *&cursor, bool &showCursor, const GuiEvent &lastGuiEvent)
{
#ifdef _XBOX
   return;
#endif

   lastGuiEvent;

   if( !getRoot() )
      return;

   
   if(getRoot()->mCursorChanged != -1 && !isMouseLocked())
   {
[b]       if(getRoot()->mCursorChanged > 100)
          return;[/b]

      // We've already changed the cursor, 
      // so set it back before we change it again.

      PlatformWindow *pWindow = static_cast<GuiCanvas*>(getRoot())->getPlatformWindow();
      AssertFatal(pWindow != NULL,"GuiControl without owning platform window!  This should not be possible.");
      PlatformCursorController *pController = pWindow->getCursorController();
      AssertFatal(pController != NULL,"PlatformWindow without an owned CursorController!");

      pController->popCursor();

      // We haven't changed it
      getRoot()->mCursorChanged = -1;
   }
   
}


One thing I am watching out for is that change I made to getCursor() to look for side effects. It's important to keep your own custom cursor IDs above 100 in order for the above modification to work. Otherwise you will see odd effects in guiEditor.


I am using arcaneFX, and the changes are immediately noticeable, when I implement the optional callback in afxSelectionMgr.cs
Canvas.addPlatformCursor("mycursor.ani",111);

function GameConnection::onObjectRollOver(%this,%object)
{
   
   //echo("Rollover : " @ %object);
   $CurrentMouseOverObject = %object;
   if(%object)
   {
      Canvas.setPlatformCursor(111);
   }
   else{
      Canvas.setPlatformCursor(0);  
   }
}

Enjoy, report any bugs or enhancements.

#1
09/24/2008 (4:13 pm)
OK, I did find a ramification from that change to guiControl::getCursor()... in the gui editor particularly where the cursor changes a lot, it now doesn't want to revert back to the original arrow :) I'll find a fix shortly.

[Edit]Found a fix, edited the function above.
#2
09/30/2008 (3:55 am)
@Dave: Thanks for the resource. :)
#3
02/25/2009 (4:08 am)
Compiles with no errors, but I can't get it to work. Can someone please point me into the right direction? I've added the following in common/gamescripts/common.cs:

// Set a default cursor.
   //Canvas.setCursor(DefaultCursor);
   Canvas.addPlatformCursor("normal.cur",111);
   Canvas.setPlatformCursor(111);

But it does nothing. I've added a debug message, to check if the lines are being executed and they are...
#4
05/20/2009 (4:45 am)
after some changes it works with TGEA 1.8.1 but due to cursor refresh default cursor which may appear & disappear very quickly.

is there a way to fix this ?

thanks

edit:
after some other tests problem appear with vista & not on xp.
#5
05/21/2009 (8:01 am)
I think the Vista thing is releated to Vista doing some type of caching on Icons and Cursors ... I recall reading something similar in another post somewhere.

Apparently if you restart your machine the problem will never show its head again.

I think Vista does caching like this to make the O/S run faster ... but don't quote me on that. :)
#6
10/21/2009 (7:39 am)
Just noting that this also works in Torque 3d.
#7
07/05/2010 (1:45 pm)
I got this to work, somewhat, in AFX 2.0 for T3D 1.1.0. The file specified for the cursor is relative to the executable, so to use the name alone, the cursor file must be located in the same folder as the executable.

If you want to hide it in a subfolder, just supply the relative path from the executable, e.g. "art/gui/mycursor.cur".

I wanted the cursor to be persistant and so I put the calls to "addPlatformCursor" and "setPlatformCursor" towards the end of the initClient function in "game/scripts/client/init.cs".

I found, however, that although the cursor worked for the menu, when clicking play, the default cursor came back. The custom cursor did not return, even if going back to the main menu.

Btw, I used Greenfish Icon Editor Pro to make my cursor file. It's a handy little free program also nice for making icon files. I don't have a stake in it, or anything; just thought it might help others.
#8
07/23/2010 (9:54 am)
OK, so I've realized that there are 9 other controls that define overrides for getCursor besides GuiControl. If you don't address those, the cursor can still be reset back to normal when encountering those contols.

To find them, search the entire solution for

::getCursor(GuiCursor
#9
07/23/2010 (11:17 am)
Also, for the function
bool PlatformCursorController::addCustomPlatformCursor(const char* cursorName, U32 cursorID)
I think it would be better to leave that as undefined and require base classes to define it like so:
bool PlatformCursorController::addCustomPlatformCursor(const char* cursorName, U32 cursorID) = 0;
Otherwise if a child class did not override it for some reason, it would call itself leading to infinite recursion.
#10
10/08/2010 (1:52 am)
Having a bit of trouble getting this into 1.8.2. I can get the echo to work, but I can't get the cursor to change. Has anyone gotten this to work in 1.8.2?
#11
11/25/2010 (11:23 pm)
anyone had any luck getting this to work in t3d beta 3? I can't seem to get it.
#12
05/18/2011 (6:15 pm)
Did you ever get this working in T3D?