Table of Contents

Lab 6 - Avatars and HUD

Before You Start

Roaming Ralph with a HUD

In this exercise you'll start with a fully working 3rd person world with Ralph as your avatar. You'll then add a couple of classic HUD features, namely the health bar and the overhead map.

  1. Setting Up:
    Get the advancedralph.py running and explore the controls, making sure you understand how to control both Ralph himself and the camera (rotating and tilting by approaching the edge of the screen with your mouse; zooming with the mouse wheel). Have the application run in Fullscreen mode, simply by changing False to True in the call to setFullscreen near the bottom of the code. It is easier to control the camera with the mouse in this mode, but the windowed mode might be better while debugging.
  2. Adding a Health Bar:
    The following code is a HealthBar object that can be attached to a scene graph. A health bar can display some fractional property in a graphical manner, such as the health or power of a character. Attach this bar to the Avatar so that the bar appears to be floating over the Avatars head (use the regular 3D scene graph). Make sure you can see it (but it may disappear when you start moving around - see below)
    class HealthBar(NodePath):
        """ Adapted from drwr's code on the Panda 3D forums """
     
        def __init__(self):
            NodePath.__init__(self, 'healthbar')
            self.group = self.attachNewNode('group')
            self.group.setPos(-0.5,0,0)
            cmfg = CardMaker('fg')
            cmfg.setFrame(0, 1, -0.1, 0.1)
            self.fg = self.group.attachNewNode(cmfg.generate())
            cmbg = CardMaker('bg')
            cmbg.setFrame(-1, 0, -0.1, 0.1)
            self.bg = self.group.attachNewNode(cmbg.generate())
            self.bg.setPos(1, 0, 0)
            self.fg.setColor(0, 1, 0, 1.0)
            self.bg.setColor(0.5, 0.5, 0.5, 1.0)
            self.value = 0.8
            self.update()
     
        def update(self):
            self.fg.setScale(self.value, 1, 1)
            self.bg.setScale(1.0 - self.value, 1, 1)        
     
        def setHealth(self, value):
            self.value = value
            self.update()
     
        def decrease(self, delta):
            if (self.value - delta) >= 0:
                self.value -= delta
                self.update()
  3. Improving the Health Bar:
    1. Since your health bar rotates with your Avatar, you may notice that it fully disappears when you're looking at the Avatar from another angle. You can use the so called Billboard effect to ensure that the health bar always faces you, no matter how the Avatar is facing. You enable this effect on any nodepath by calling its setBillboardAxis method. Try using this to make your health bar always visible.
    2. The healthbar calls a little too much attention to itself on the display. Make it semi-transparent so that it blends in better. Don't forget to enable transparency in the scene graph (HINT: setTransparency).
  4. Displaying Dynamic Health:
    Now make the healthbar start decreasing while the Avatar is running and then gain full health when the Avatar stops running. HINT: For any FSM class, you can query the state member variable to see what state it is in.
  5. Adding a Sliding HUD Panel:
    So far you have been adding visual information to the 3D scene, but let's create a simple HUD (Heads-Up-Display) panel that stays in front of the 3D scene, no matter where you are looking. For this construction, start filling in the setupHUD() method in World. You will actually just create a single HUD panel that will contain two elements: (1) A button that makes the panel slide up and down, and (2) an overhead map that works like a GPS. First, get the panel showing and the panel sliding button working. Create a new node under “aspect2d” that you can call panel. You attach everything to this node that you want sliding with it.
    1. Attach the panel background image to the panel node. You can create the panel image as a node in a tree with a single call to OnScreenImage - you set its parent with the parent attribute (otherwise the parent defaults to aspect2d). Create the panel image with the texture called panel.tif and scale it to (0.36,1,0.40). Within the World redraw method, set the position of this panel to (-base.getAspectRatio()+0.36,0,-0.60).
    2. Attach the panel button to the panel node. You can use the following button specification:
              <your button> = DirectButton(
                  text_fg=(1,1,1,1),
                  text_shadow=(0,0,0,1),
                  frameColor=(0.4,0.5,0.4,1),
                  borderWidth=(0.1,0.1),
                  relief=DGG.RAISED,
                  text=("Map","Map","> Map <",""),
                  pressEffect=1,
                  scale=0.035,
                  frameSize=(-3,3,-0.5,1.0),
                  command= <your function here>,
                  rolloverSound = <your sound here>,
                  clickSound = <your sound here>, 
                  parent=<parent node here>)

      You can use the sounds from this week's gui demo or simply not specify new sounds to use the default Panda3D sounds. The function that the button calls, should be a function that toggles the HUD panel between fully visible and mostly hidden. There's already a toggleHUD method stub you could use. Remember that you have to set the button position in the redraw method. A good position of the button is (-base.getAspectRatio()+0.535,0,-0.26).

    3. Set up a couple of intervals that will slide the panel in and out of view. You can make it slide down until all you can see is the button. Fill in the toggleHUD method to start the correct interval when the button is pressed.
  6. Adding an Overhead Map:
    The following code is an OverheadMap object that can be attached to a scene graph. Add this object to your panel and have it show you where your avatar is located any given moment. It is good for you to know that the world dimensions in the x-y plane are approximately (-128,-68,48,48). You will need to scale and reposition your map until it fits the panel depression. Also, to enable transparency on the 2D scene graph, you'll need to call setTransparency again because it is a separate scene graph from render. Make sure this is all working fine.
    class OverheadMap(NodePath):
     
        def __init__(self, x1, y1, x2, y2):
            """ Create a square map with the given world corners in the x-y plane """
            NodePath.__init__(self, 'overheadmap')
     
            self.setBoundary(x1,y1,x2,y2)
     
            cmbg = CardMaker('bg')
            cmbg.setFrame(0,1,0,1)
            self.prime = self.attachNewNode(cmbg.generate())
            self.prime.setColor(0.5,0.7,0.5,0.1)
     
            cmav = CardMaker('am')
            cmav.setFrame(-0.02,0.02,-0.02,0.02)
            self.avatar = self.prime.attachNewNode(cmav.generate())
            self.avatar.setPos(0,0,0)
            self.avatar.setColor(0,1,0,1)  
     
        def addLandmarks(self, landmarks, color=Vec4(1,0,0,0.5)):
            """ Take in a list of (x,y) touples in world coordinates and place them
                as little dots on the map in the given color """
            ## To be filled in...
     
        def setBoundary(self, x1, y1, x2, y2):
            """ Define the extent of the map in world coordinates.  Create the proper
                offset and scaling values that transform world coordinates to map coordinates"""
            self.xoffset = x1
            self.xscale = 1.0/(x2-x1)
            self.yoffset = y1
            self.yscale = 1.0/(y2-y1)
     
        def setAvatarPos(self, x, y):
            """ Sets the position of the avatar marker to the given world coordinates """
            self.avatar.setPos((x-self.xoffset)*self.xscale,-0.01,(y-self.yoffset)*self.yscale)
  7. Populating the Overhead Map:
    Now add some landmarks to your world and have them show up on your map as little red squares. You can use the Models/box.egg object. Initialize a few of these boxes by storing a list of (x,y) tuples and then iterate through the list to place them in the world. You should also be able to send the list directly into the map's addLandmarks method and have the locations of the boxes light up. You'll need to fill in the rest of that method. Test to see if this is all working.
  8. Make it a Game:
    If you feel adventurous, change this application into a game where the health bar starts to drop as time passes and the only way to regain full health is to touch a box, which then disappears. You win the game if you reach all the boxes. By randomizing the initial box locations, you'll really need the map to help you play this game.