BGE: Custom Cursor

Blendenzo’s custom cursor tutorial does a great job at providing us with a simple way to add custom cursors to our games. But what if we want it to do more? What if we want to control the sensitivity, or the area that the cursor is in. Well, read on as we return to our Duck Duck Moose game to improve the cursor control with a custom cursor class.

We’ll start a bit backwards, by looking through cursor.py and what it does before moving on to how to use it. I placed everything in it’s own class that extends the KX_GameObject so that it can easily be applied to a number of games and the class can be extended to suit needs. It’s designed to be passed the KX_GameObject that is functioning as a cursor.

cursor.py:

##################################################################
#                                                                #
# BGE Custom Cursor v1                                           #
#                                                                #
# By Jay (Battery)                                               #
#                                                                #
# https://whatjaysaid.wordpress.com/                              #
#                                                                #
# Feel free to use this as you wish, but please keep this header #
#                                                                #
##################################################################

from bge import render, logic, types, events

class Cursor (types.KX_GameObject):
    def __init__(self, own):
        self._searchScene = logic.getCurrentScene()
        self.camera = logic.getCurrentScene().active_camera
        self.sens = 0.005
        self.screenX = render.getWindowWidth()
        self.screenY = render.getWindowHeight()
        self.centreX = int(self.screenX/2)
        self.centreY = int(self.screenY/2)
        self.searchScene = logic.getCurrentScene()
        render.setMousePosition(self.centreX, self.centreY)
        self.cursorPos = [0.5,0.5]
        self._limits = [0,0,1,1]
        return

    @property
    def searchScene(self):
        return self._searchScene

    @searchScene.setter
    def searchScene(self, scene):
        for s in logic.getSceneList():
            if s.name == scene:
                self._searchScene = s

    @property
    def limits(self):
        return self._limits

    @limits.setter
    def limits(self, limitList):
        assert isinstance(limitList, (list, tuple)), "Limits must be a list"
        assert len(limitList) == 4, "limits takes a list of 4 integers [x1,y1,x2,y2]"
        self._limits = [limitList[0]*0.01, limitList[1]*0.01, 1-(limitList[2]*0.01), 1-(limitList[3]*0.01)]

    def centreRealCursor(self):
        render.setMousePosition(self.centreX, self.centreY)
        return

    def getMovement(self):
        mPos = logic.mouse.position
        x = self.centreX - (mPos[0]*self.screenX)
        y = self.centreY - (mPos[1]*self.screenY)
        self.centreRealCursor()
        return [x, y]

    def moveCursor(self):
        movement = self.getMovement()
        movement[0] *= self.sens
        movement[1] *= self.sens
        self.position.x -= movement[0]
        self.position.y += movement[1]
        self.cursorPos = self.camera.getScreenPosition(self)
        if self.cursorPos[0] > self.limits[2] or self.cursorPos[0] < self.limits[0]:
            self.position.x += movement[0]
        if self.cursorPos[1] > self.limits[3] or self.cursorPos[1] < self.limits[1]:
            self.position.y -= movement[1]
        return

    def getCursorOver(self, prop=""):
        cam = self.searchScene.active_camera
        obj = cam.getScreenRay(self.cursorPos[0], self.cursorPos[1], 1000, prop)
        return obj

    def mouseEvents(self):
        if logic.mouse.events[events.MOUSEX] == logic.KX_INPUT_ACTIVE:
            self.onCursorMovement()
        if logic.mouse.events[events.MOUSEY] == logic.KX_INPUT_ACTIVE:
            self.onCursorMovement()
        if logic.mouse.events[events.LEFTMOUSE] == logic.KX_INPUT_JUST_ACTIVATED:
            self.onLeftClick()
        if logic.mouse.events[events.RIGHTMOUSE] == logic.KX_INPUT_JUST_ACTIVATED:
            self.onRightClick()
        if logic.mouse.events[events.MIDDLEMOUSE] == logic.KX_INPUT_JUST_ACTIVATED:
            self.onMiddleClick()
        if logic.mouse.events[events.WHEELUPMOUSE] == logic.KX_INPUT_JUST_ACTIVATED:
            self.onWheelUp()
        if logic.mouse.events[events.WHEELDOWNMOUSE] == logic.KX_INPUT_JUST_ACTIVATED:
            self.onWheelDown()
        return
    def onCursorMovement(self):
        self.moveCursor()
    def onLeftClick(self):
        pass
    def onRightClick(self):
        pass
    def onMiddleClick(self):
        pass
    def onWheelUp(self):
        pass
    def onWheelDown(self):
        pass

We treat the cursor exactly like we would if it was a first person camera: keep the mouse cursor centred and measure movement from the centre position. We can then multiply the distance moved by a sensitivity value (self.sens) and add the result to the pseudo-cursor’s position. This task is handled by the methods .centreRealCursor(), .getMovement() and .moveCursor().

For the pseudo-cursor to function as a real cursor we need to keep track of it’s position in the screen space. This is done using the KX_Camera method .getScreenPosition() and is saved in the class variable self.cursorPos[x,y]. The main work is done in .mouseEvents(). This method looks for active events and applies the appropriate function. The methods for events are deliberately left blank so that you can define the action that occurs (we’ll see how later) as each game will handle events differently. The only exception is .onCursorMovement(), which is bound to .moveCursor() unless otherwise specified by the user.

For the cursor to interact with the scene the .getCursorOver(prop=””) method is able to return the object that the cursor is over, or None if there isn’t one. You can pass a property so it only picks up objects with that property.

Now, here’s where things get a little tricky. If the script is running in an overlay scene, then, by default, the .getCursorOver() method will look in that scene. To specify what scene we want to look for mouse over objects we need to see the variable self.searchScene to the scene we’re after. This just takes the scene’s name as a string, the class will do the rest of the work. Finally, this only works with a scene that has a camera and the camera must be perspective. Orthographic cameras will not return the correct object the mouse is over (this is down to a bug in blender). By being able to set the search scene, we can keep our overlay camera in perspective mode, while allowing us to make sure we’re looking in the right scene.

The Cursor() class allows for the setting of limits for the cursor’s operation. By default, this is the game screen. We can set them by assigning a list of 4 integers to the property .limits ( ie. myCursor.limits = [x1,y1,x2,y2]). These values are percentages of screen space. So .limits = [10,10,10,10] would create a boarder 10% of the screen size where the cursor cannot enter. Using percentages means that the cursor’s area of operation remains the same regardless of the screen size.

Setting it up

Create a new scene, we’ll call it ‘HUD’. In this scene add your cursor object. Make sure that the front of the cursor (what the player sees) is facing up along the world’s Z axis. Now add a camera. Position your camera above the cursor object so it’s looking down the Z axis.

All going well, when you look down the camera the Y axis should be up/down for the cursor and the X axis should be left/right.

Using the Cursor.py module

Create a new script. Let’s call this runCursor.py:

import bge
import cursor

cont = bge.logic.getCurrentController()
own = cont.owner
own = cursor.Cursor(own)
own['over'] = None

# Define event actions
def leftClick():
    obj = own.getCursorOver()
    if obj:
        obj['clicked'] = True

def hover():
    own.moveCursor()
    obj = own.getCursorOver()
    if obj and 'hover' in obj:
        obj.children[0].visible = True
        own['over'] = obj
    if own['over'] != obj:
        if own['over']:
            own['over'].children[0].visible = False
        own['over'] = obj

own.onLeftClick = leftClick

# run by cursor object
def run():
    own.mouseEvents()

# change cursor states
def changeSceneToMenu():
    own.searchScene = bge.logic.getCurrentScene().name
    own.limits = [0,0,0,0]
    own.onCursorMovement = hover
    own['over'] = None

def changeSceneToGame():
    own.onCursorMovement = own.moveCursor
    own.searchScene = 'Game'
    own.limits = [22,5,22,40]

We can now import cursor.py and create a new cursor object. In doing so we pass the KX_GameObject to the Cursor() class and assign the value back to own. We can still access all the usual game object methods through own, only now it can also use the Cursor() methods too.

First, we define some event actions. The first is a left click. Here, we use the .getCursorOver() method to grab an object that has been clicked on. There are now numerous things we could do with this object. In this example, it sets a property on the game object to True so the object knows that it has been clicked on and can respond appropriately (in the duck duck moose game this is done through the use of property sensors). Finally, we assign our new leftClick() function to the cursor’s left click function (line 26).

We’ve also got a hover action. This combines the .moveCursor() method along with the .getCursorOver() method to act on the object being hovered over. We don’t bind this to any event just yet.

This script is designed to be run in module mode, so we create a function called run() that just calls the cursor’s mouseEvents() function.

The last 2 functions are designed to be called when the scene changes, by the new scene in a python module controller. Because the script is running in module mode own is still the cursor object. This sets up the context for the cursor’s operation. So when we’re on a menu screen we’ve removed any screen limits and set the .onCursorMovement() method to the new hover function we defined earlier. When we switch to the game scene we change .onCursorMovement() back to the .moveCursor() method. Each time, we’re making sure to update .searchScene. Using these functions we can easily use one instance of the Cursor class to respond to a variety of situations.

Setting up logic bricks

The last step is to select the cursor object and add a python module controller to a sensor, like this:

cursorSetUp3

I used an always sensor to trigger once to initialise the module, then the run() function we defined earlier will be called each time the mouse moves. You could just use an always sense set to true pulse mode as the Cursor.py module does not require any particular sensors/actuators.

And that’s all there is to it. A completely, customisable, extendible, programmable cursor for the BGE. Have a look at the Duck Duck Moose game in the resources section below to see it in action.

Other ways to use cursor.py

Because all the magic is contained within a module you can import it anywhere. This means you can control your cursor from outside the overlay scene and just pass it the cursor object on initialisation. To do this, set the cursor overlay scene up as described above without any logic/python. Then in your main scene your python can look something like this:

import bge
import cursor

cont = bge.logic.getCurrentController()
own = cont.owner
scene = bge.logic.getCurrentScene()

def leftClick():
    obj = myCursor.getCursorOver()
    print(obj)
    if obj:
        #do something with the object, like:
        obj.endObject()
    
if 'cursor' in own:        
    own['cursor'].mouseEvents()
else:   
    for s in bge.logic.getSceneList():
        if s.name == "HUD":   
            myCursor = s.objects['Cursor'] 
            myCursor = cursor.Cursor(myCursor)
            #the cursor's camera must be set to the overlay scene camera
            myCursor.camera = s.active_camera
            myCursor.onLeftClick = leftClick
            own['cursor'] = myCursor
            break

Then, providing you’re regularly running myCursor.mouseEvents() it’ll all work fine. This will have the advantage of having the object returned by myCursor.getCursorOver() being in the same script as other parts of the game code. This means that you don’t need to find a way to tell the object/game that something has been clicked on.

However, when using this method you need to be aware of two things. Firstly, you need to make sure that the overlay scene has been initialised before trying to pass the cursor class the cursor game object. Secondly, you need to update Cursor.camera to the camera in the overlay scene (line 23 in the above example), otherwise the cursor position will be wrong.

Alternatively, you could set the game up just like in the above tutorial, with the all the cusor control happening in the overlay scene, and extend the KX_GameObject class to handle click events:

from bge import types, logic

class Button (types.KX_GameObject):
    # extends KX_GameObject to add on click events
    def __init__(self, own):        
        return
    
    def onClick(self):
        pass
    
    def inflate(self):
        self.localScale[0] += 0.2
        self.localScale[1] += 0.2
        self.localScale[2] += 0.2
        return
    
    def deflate(self):
        self.localScale[0] -= 0.2
        self.localScale[1] -= 0.2
        self.localScale[2] -= 0.2
        return

def mutateInflate():
    own = logic.getCurrentController().owner
    own = Button(own)
    own.onClick = own.inflate
    own['clickable'] = True
    
def mutateDeflate():
    own = logic.getCurrentController().owner
    own = Button(own)    
    own.onClick = own.deflate
    own['clickable'] = True

This script would be attached to a python module and call the appropriate mutate function for each object. Then runCursor.py would look something like this:

import bge
import cursor

cont = bge.logic.getCurrentController()
own = cont.owner
own = cursor.Cursor(own)

# Define event actions
def leftClick():
    obj = own.getCursorOver()
    if obj:
        if 'clickable' in obj:
            obj.onClick()

own.searchScene = 'Scene'
own.onLeftClick = leftClick

# run by cursor object
def run():
    own.mouseEvents()

See the resources section below for examples of other ways to use cursor.py

Closing Remarks

Hopefully, this custom cursor module is useful to people. There’s some bugs to fix and I plan to expand on it and increase it’s functionality (switching cursors, actually implement some error handling, more user control) so will add a post with the latest version when I do. I have other plans for using this cursor object as a base class for a more complex cursor system in a RTS game and will but forward a tutorial on it soon.

Until then, any comments, requests for features, questions or noticed any errors, leave a note below. And don’t forget to check out the Blender page for other BGE/Python tutorials.

Resources

Duck Duck Moose game

Using cursor.py outside of an overlay scene .blend

Extending KX_GameObject to handle on click evets .blend

Blendenzo’s original custom cursor tutorial

Advertisements

~ by Jay on May 5, 2014.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

 
%d bloggers like this: