Tutorial: Adding a GUI to your application

From FIFE development wiki
(Redirected from Tutorial3)
Jump to: navigation, search

Introduction

In this tutorial, we will assume that you have read the two previous tutorial, you understand how to create an application, you were able to write a game controller and load a map with it. It this not the case yet, feel free to read them :

You are still there, means that you are ready? Ok, let's make our first interactive application! :-)

A FIFE map in detail

Ok so normally, at this point, you know that a map is nothing but a xml file. You know how to create it and load it in your game. If you have already tried to create your own map and load it, you surely already seen that you have to set layers and cameras with the editor.

Let's consider this diagram :

Fife map.png

This is pretty explicit. A map is split into layers. Basically, in the editor, you create a layer for each stage of your map. You start by the lower stage (ground for example) and create a new layer for each upper stage of your map (NPC's is an example). But this tutorial is not about using the editor so we will assume that you have an already created map (take the map of 'Rio de hola' demo for the rest of the tutorial).

Back in our game controller

So let's take our basic game controller we wrote in the previous tutorial :

class GameController(object):
        '''
        Main game class. It handles all the game process.
        '''
 
        def __init__(self, application, engine, settings):
                self.application = application
                self.engine = engine
                self.model = self.engine.getModel()
                self.settings = settings
 
                self.listener = GameListener(self.engine, self) #game listener                                                                                                      
 
                #new stuffs                                                                                                                                                         
                self.filename = None                                                                                                                                                
                self.map = None                                                                                                                                                     
 
        def loadMap(self, filename):                                                                                                                                                
                '''
                Load a map from a xml file.
                '''
 
                self.reset()
 
                self.filename = filename
 
                self.map = loadMapFile(os.path.join('maps', filename + '.xml'), self.engine, extensions = {'lights': True})
 
        def reset(self):
                '''
                Reset the current map informations.
                '''
 
                if self.map:
                        self.engine.getModel().deleteMap(self.map)
 
                retval = self.engine.getModel().deleteObjects()
 
                self.map = None
                self.filename = None

Ok so right after the call :

self.map = loadMapFile(os.path.join('maps', filename + '.xml'), self.engine, extensions = {'lights': True})

our 'self.map' attribute will be a Map object. Might be a good introduction to python API reference of FIFE. Let's take a look at : http://fifedocs.boxbox.org//epydoc/. Choose fife.fife module in the top left menu and then choose the Map class in the bottom left one.

As you can see, it displays all the available methods provided by this class. If you scroll a bit you will find the getLayer method we will use it to get the layers of our map.

getLayer() is really simple to use, it just need a string that is the name of the layer you want to get and then return a Layer object. The 'Rio de hola' map use three different layers :

  • TechdemoMapTileLayer
  • GroundLayer
  • TechdemoMapGroundObjectLayer

Take a look at this part of the shrine.xml map file of the demo :

<layer x_offset="0.0" pathing="cell_edges_and_diagonals" y_offset="0.0" grid_type="square" id="TechdemoMapGroundObjectLayer" transparency="0" x_scale="0.5" y_scale="0.5" rotation="0.0">
                <instances>
                        <i x="8.0" o="shrine" z="0.0" y="-1.0" r="180"></i>
                        <i x="11.0" o="shrine" z="0.0" y="-1.0" r="0"></i>
                        <i x="68.0" o="rocks:01" z="0.0" y="17.0" r="0"></i>
                        ...
                </instances>
</layer>

It shows the map separation as the previous diagram (see on top of this tutorial). Don't care of all the attributes, this file is generate by the editor anyways, just focused on how a map is made, it will be really useful when you will use the map in your game.

So for the rest of the tutorial we will work with the 'TechdemoMapGroundObjectLayer'. Let's have a look at the end of the shrine.xml file :

<i x="5.0" o="boy" y="4.0" r="0" id="PC" z="0.0"></i>
<i x="8.0" o="girl" y="2.0" r="0" id="NPC:girl" z="0.0"></i>

Here we are two instances of our layer. The differences with the other instances we just saw above is the 'id' attribute. This is this attribute we will use for simply get a specific instance in our game :

instance = ourlayer.getInstance("instance_id")

Let's summarize a bit and modify our loadMap method :

        def loadMap(self, filename):
                '''
                Load a map from a xml file.
                '''
 
                self.reset()
 
                self.filename = filename
 
                self.map = loadMapFile(os.path.join('maps', filename + '.xml'), self.engine, extensions = {'lights': True})
 
                self.layer = self.map.getLayer('TechdemoMapGroundObjectLayer') #here we create our Layer object
 
                self.player_instance = self.layer.getInstance('PC') #here we grab the 'PC' instance of our layer.

Also don't forget to modify your __init__ method and add those lines in reset :

self.layer = None
self.player_instance = None

Ok so now we have the instance of the hero of our game, an Instance object (never forget the API reference that you already bookmarked). What know?

Add an action listener

Now you have the Interface object of your hero and thanks to it we will be able to make this hero living!

Always in the API reference, we have the addActionListener() method it will be use to bind our action listener with our instance. So now we have to write an action listener :

It always inherits from the fife.InstanceActionListener class :

class Hero(InstanceActionListener):
        def __init__(self, player_instance):
                fife.InstanceActionListener.__init__(self)
                self.player_instance = player_instance
 
                self.player_instance.addActionListener(self) #bind the action listener to the instance
 
        def onInstanceActionFinished(self, instance, action):
                self.idle()
 
        def start(self):
                self.idle()
 
        def idle(self):
                self.player_instance.act('stand', self.player_instance.getFacingLocation())
 
        def run(self, location):
                self.player_instance.move('run', location, 4 * 1)

The only method provided by InstanceActionListener class is onInstanceActionFinished. It's called when no action are in progress, so we can use it to do some fancy things like implementing a counter and hit the player when he is inactive since a while.

Let's get focused on idle() and run(). Those two methods are the actions available for our hero. They are really simple since they respectively call act and move methods provided by Instance. If you check them out in the API reference :

act(self, string action_name, Location direction, bool repeating = False)
  • the action_name argument is the id of the animation to play with this action. An animation is nothing but an xml file, this is not the topic of this tutorial so we will use the 'stand' and 'run' action from 'Rio de hola' demo.
  • direction is a Location object that represent the direction of the instance
  • repeating : a boolean set on True if this action has to be repeated
move(self, string action_name, Location target, double speed)

This method is awesome since you can use it to move your instance (our hero) to a given location, it handles path-finding etc... just need to provide the target location of the deplacement.

  • action_name : the same as act
  • target : a Location object that represent the target location
  • speed : the speed of the instance during the deplacement

Ok let's make a break and summarize all we saw :

The game controller :

class GameController(object):
        '''
        Main game class. It handles all the game process.
        '''
 
        def __init__(self, application, engine, settings):
                self.application = application
                self.engine = engine
                self.model = self.engine.getModel()
                self.settings = settings
 
                self.listener = GameListener(self.engine, self) #game listener                                                                                                      
 
                #new stuffs                                                                                                                                                         
                self.filename = None                                                                                                                                                
                self.map = None  
 
                self.layer = None          
 
                self.player_instance = None
 
                self.player = None                                                                                                                                         
 
        def loadMap(self, filename):
                '''
                Load a map from a xml file.
                '''
 
                self.reset()
 
                self.filename = filename
 
                self.map = loadMapFile(os.path.join('maps', filename + '.xml'), self.engine, extensions = {'lights': True})
 
                self.layer = self.map.getLayer('TechdemoMapGroundObjectLayer') #here we create our Layer object
 
                self.player_instance = self.layer.getInstance('PC') #here we grab the 'PC' instance of our layer.
 
                self.player = Hero(self.player_instance) #here we create the action listener and pass the player instance to it
                self.player.start() #start it
 
        def reset(self):
                '''
                Reset the current map informations.
                '''
 
                if self.map:
                        self.engine.getModel().deleteMap(self.map)
 
                retval = self.engine.getModel().deleteObjects()
 
                self.map = None
                self.filename = None

The action listener :

class Hero(InstanceActionListener):
        def __init__(self, player_instance):
                fife.InstanceActionListener.__init__(self)
                self.player_instance = player_instance
 
                self.player_instance.addActionListener(self) #bind the action listener to the instance
 
        def onInstanceActionFinished(self, instance, action):
                self.idle()
 
        def start(self):
                self.idle()
 
        def idle(self):
                self.player_instance.act('stand', self.player_instance.getFacingLocation())
 
        def run(self, location):
                self.player_instance.move('run', location, 4 * 1)

Whew! So know we have our instance, we have bound an action listener on it and now? Done? Not yet sorry :-)

We know that the hero can run and idle (cf action listener). The last issue to deal with is where and when call the run function. If you are attentive reader you should have seen that the run() method need a Location object to pass to the move() one, and this Location object will be the target location of our player instance. A Location object, to be simple, represent the coordinate of something on the isometric graph.

The idea of the next section is to grab a Location object that represent the position on the graph of a mouse click somewhere the screen and then use this Location as the target for the run method().

It will be the occasion to introduce a new concept : the event listener and the cameras

Add cameras

Once again, there are defined in the edition process of the map and we will have to use them in our game. 'shrine' map of 'Rio de hola' demo implements two cameras : 'main' and 'small'. We will only use 'main' camera.

We write a new method to call in our loadMap method :

        def initCamera(self):
                '''
                Set the camera.
                '''
 
                camera_prefix = self.filename.rpartition('.')[0] # Remove file extension
                camera_prefix = camera_prefix.rpartition('/')[2] # Remove path
                camera_prefix += '_'
 
                for cam in self.map.getCameras():
                        camera_id = cam.getId().replace(camera_prefix, '')
 
                        self.cameras[camera_id] = cam
                        cam.resetRenderers()
 
                self.cameras['main'].attach(self.player_instance)

This function will grab the cameras in the xml map file and save then in a dictionnary. The last line is the most important, we attach the 'main' camera on our hero instance, then when our hero will be able to run, the camera will 'follow' him.

Now we load cameras, so we also need to reset them. reset() method need to be updated :

self.cameras = {}

Write an Event Listener

We want to listen for mouse events only, so our class have to inherits from IMouseListener from fife module (API Reference).

class GameListener(fife.IMouseListener):
        '''
        The game listener.
 
        Handle mouse events in relation
        with game process.
        '''
 
        def __init__(self, engine, gamecontroller):
                self.engine = engine
                self.gamecontroller = gamecontroller
 
                self.eventmanager = self.engine.getEventManager()
 
                super(GameListener, self).__init__()
 
        def mousePressed(self, event):
                pass
 
        def mouseMoved(self, event):
                pass
 
        def mouseReleased(self, event):
                pass
 
        def mouseDragged(self, event):
                pass

We override all the methods provided by IMouseListener but we will use only one of them :

        def mousePressed(self, event):
            pass

This is called when one of the mouse buttons if pressed.

Customize the mousePressed method

       def mousePressed(self, event):
                if event.isConsumedByWidgets():
                        return
 
                clickpoint = fife.ScreenPoint(event.getX(), event.getY())
 
                if (event.getButton() == fife.MouseEvent.LEFT):
                        self.gamecontroller.player.run(#pass the target location here)

Let's consider the mousePressed method above. The first condition test if this mouse event have already been consumed by the widgets (wait for the GUI tutorial to understand).

clickpoint = fife.ScreenPoint(event.getX(), event.getY())

When this method is called, the event argument is a MouseEvent object (cf API Reference) that provides getX() and getY() methods to grab the position of the click on the screen and we use them to create a ScreenPoint object.


The following condition test if the mouse button pressed is the left one, if this is the case, we move our hero.

Now we would like to get the equivalent Location object of the mouse click, 'Rio de hola demo' provides a method to do it :

        def getLocationAt(self, clickpoint):
                '''
                Get the map location that the screen
                point refers to.
                '''
 
                target_mapcoord = self.cameras['main'].toMapCoordinates(clickpoint, False)
                target_mapcoord.z = 0
 
                location = fife.Location(self.agentlayer)
                location.setMapCoordinates(target_mapcoord)
 
                return location

The topMapCoordinates method of the Camera class take a ScreenPoint argument and return an ExactModelCoordinate that represent a position on the isometric graph. We create a new location and use setMapCoordinates() to place it the isometric environment.

We now have the location that correspond of the mouse click point, so we can update our mousePressed method :

self.gamecontroller.player.run(self.gamecontroller.getLocationAt(clickpoint))

And then the hero will move toward his new location.

Conclusion

After that you should have a game where you can move a character across the map. We saw lot of FIFE concepts and the last one is about gui in your FIFE application. Now you should be able to progress by yourself in the FIFE game creation world :)