December 12, 2010

ISSUE 4: What goes up…

7. Gravity

Currently if you slow down to a stop in the game you will find your player just hovers. Stationary and levitating. It is time now to add some basic physics to the game – starting with gravity, to pull the player down to the ground. Our gravity needs to be relative to speed too – the faster you fly the more lift you create (it’s how planes work!) so the gravity is effectively less. With that in mind…

Panda3D ships with support for several Physics libraries. Firstly, it has its own built in library, comparably basic it provides support for applying forces to objects. Next, there is the Open Dynamic Engine (ODE) which allows for far more complex simulation. Lastly, support for NVIDIA PhysX was added from Panda3D 1.7.0 onwards but is not (as of yet) as well documented.

So which are we to use? For our purposes – an arcade dogfight game – at this stage in the development – the answer is “none of the above”. We would not only need to setup a gravity simulation, but also simulate aircraft lift. If we were writing a simulator we would be recommending to go through all of this, but for a basic arcade game we can achieve the result we want in two lines of code!

(worry not, if you were wondering – yes, we will most certainly be revisiting the Panda3D Physics options in the future!)

At the start of your updatePlayer method, right after the ‘factor’ variables are created, add a new one:

        gravityfactor = ((self.maxspeed-self.speed)/100.0)*2.0

Feel free to play with the values to see what impact it has on the game. Now, at the end of your updatePlayer method, update the code to look like this:

        if self.exploding == False:
            self.player.setX(self.player,-speedfactor)
            self.applyBoundaries()
            self.player.setZ(self.player,-gravityfactor)

The last line (setZ) is the new line. If you run your game now you will find the player gradually drops to the ground. The slower you fly, the faster you drop. Fly up to top speed and you won’t drop at all. That’s it! Two lines of code, one slightly more playable game!

8. It’s a kind of magic…

Here’s a quick change with noticeable impact. Change your updateCamera method to look like this:

    def updateCamera(self):
        # see issue content for how we calculated these:
        percent = (self.speed/self.maxspeed)
        self.camera.setPos(self.player, 19.6225+(10*percent), 3.8807, 10.2779)
        self.camera.setHpr(self.player,94.8996,-12.6549, 1.55508)

Notice we are positioning the camera relative to the players speed. The result? As you fly faster the player model moves further away from the camera. Slow down and the camera gets closer to the player. The screen shots below show this in action, the first is low speed, the second high:

Camera Tricks

9. Lighting, Fog, the Sky and Node Paths

We ended Issue 3 by adding lighting, fog and a sky. We didn’t explain how each worked in detail then, but now is the time! We also noted some problems with our sky that we will address now.

As we noted previously, there are 3 main options to consider for a Sky. The sky dome, the sky sphere, the cube map. A dome is exactly that (refer back one page, we showed the sky dome in our discussion of OOBE). A sphere is also as you would expect – a large ball painted with a sky texture on the inside, we place our world (or at least the visible world) within the sphere. A cube is similar but uses 6 images instead of one (one for each face of the cube), quite useful for more varying skies or scenarios where you want a visually different floor/ceiling.

Types of Game Sky

In Issue 3 we opted for the sky dome. You have already seen problems with it if you fly high or near to the end of your world. The sky dome is the easiest solution but is just not suited to a flight game. Had we been working on a platform game or any other game where the player is typically ground-based it would likely have been fine. To seal up the gaps in our world are going to switch to a sky sphere. Welcome to the world of trial and error! 🙂

The Panda3D web site offers a free ‘art pack’. It contains hundreds of models of varying quality. Sometimes, you’ll find the ‘perfect model’ in this pack. Sometimes, you might find something that is perfect as a ‘place holder’ to be replaced later. Luckily for us, it includes a nice sky sphere that pretty much ‘just works’ for our game.

You can download the entire Art Pack by clicking here. You need to then extract the ‘alice skies blue skysphere’ model in the ‘cat-skies’ folder (directory).

If you just want the actual sky sphere, you will need the model (egg) file and the texture (image) file. You can download them using the links below:

Save the files in your models directory. You can delete the old sky if you like or archive it for future use and/or reference. Changing the sky changes the environment. That also results in us needing to tweak our fog and lighting. Update your createEnvironment method to look as follows:

    def createEnvironment(self):
        # Fog
        expfog = Fog("scene-wide-fog")
        expfog.setColor(0.5,0.5,0.5)
        expfog.setExpDensity(0.002)
        self.render.setFog(expfog)

        # Our sky
        skysphere = self.loader.loadModel('models/blue-sky-sphere')
        skysphere.setEffect(CompassEffect.make(self.render))
        skysphere.setScale(0.08)

        # NOT render or you'll fly through the sky!:
        skysphere.reparentTo(self.camera) 

        # Our lighting
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor(Vec4(.6, .6, .6, 1))
        self.render.setLight(self.render.attachNewNode(ambientLight))

        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setColor(VBase4(0.8, 0.8, 0.5, 1))
        dlnp = self.render.attachNewNode(directionalLight)
        dlnp.setPos(0,0,260)
        dlnp.lookAt(self.player)
        self.render.setLight(dlnp)

The Fog

Remember, we added fog to help blend the scene because we cropped the cameras maximum distance to improve performance back in Issue 3. Having said that, the real purpose of fog is a nice visual effect. Our code creates a Fog object. It then sets the Fog Color – 3 values are given. One for red, one for green, one for blue. You can make any colour mixing the three. The RGB values are floating point values (floats have a decimal point; integers do not) between zero and one. 0,0,0 is black. 1,1,1 is white. 1,0,0 is pure red. Go ahead and play with the values. We then set the fog density before re-parenting to render. Again, we encourage you to change the values, particularly if you have changed the ‘maxdistance’ variable in your constructor (we set ours to 400).

The Sky

We load our new sphere, scale and parent to camera. We add an effect – the CompassEffect. It is because of this that the sky doesn’t twist and turn with the camera. That would have looked strange! This is akin to leaving the sky parented to the camera – but break their tie in terms of orientation (sell below section on node paths).

The Lighting

Panda3D offers a number of lighting types. We are using two (ambient and directional). The types of light available are:

  • Ambient light. If you go outside on a cloudy day, that’s ambient light. There does not appear to be a ‘source’ of the light but it is present and consistent. Think of it as base-lighting.
  • Directional light. Like the sun. A directional light has a direction. It ‘shines’ infinitely in that direction.
  • A Point light. Like a light bulb. Light originates from a single point infinitely in all directions.
  • Spotlights. A spotlight has both a point and a direction, a field of view and a lens. The most complicated type of light on offer.

On to our code. We create an ambient light object, then we set the colour of the light. Interestingly, the method takes a Vec4 (4-point vector). Red, Green, Blue and Alpha (transparency). The alpha is largely irrelevant for lighting, we set it to 1 (no transparency). The colour values for a light can also be above 1 to make the light brighter. After creating the light we make a ‘nested-call’. The line:

        self.render.setLight(self.render.attachNewNode(ambientLight))

We could have written this instead:

        somevariable = self.render.attachNewNode(ambientLight)
        self.render.setLight(somevariable)

Instead, we condensed down to one line of code and avoided a variable (they all do take up a little memory!) by using nested calls. As to what the code does and we need to explain that via explaining node paths. So what’s a node path? Think of a node path as a tree you can navigate. A NodePath is actually a handle to PandaNodes. Your whole scene is made up of PandaNodes (of which there are several different types – ModellNode, LightNode etc.). These are organised into a tree, your Scene Graph.

To start traversing that tree from ‘the point you choose’ you get a ‘handle’, a NodePath, to that part of the tree. The root of the tree is ‘render’. The diagram below should clarify (click the image to enlarge it).

The Scene Graph

Think of the tree in terms of parents and children. Render is the top-level parent (root). Where the parent goes, the children follow. That’s why our sky dome worked. If you looked at the dome in OOBE mode you would have seen it was actually quite small! Much smaller than the world. Yet the sky worked because it was re-parented to the camera. When the camera moved, so did the sky dome, so its size was never revealed to the player (aside from, as we saw, flying high or far). The same applies to scale, orientation, etc. Put another way, if you call a method on a parent node, it’s the same as calling it on all of its child nodes too.

It is also worth noting that we can, if we wish, create blank NodePaths for no purpose other than logistically grouping other nodes.

Back to our lighting. 🙂 We attached the ambient light node to the scene graph but got a handle back (a node path). We then told render that it was a light node via setLight.

We then create our directional light. A similar process. We grab the variable from attachNewNode this time instead of a nested call as we did with ambient lighting. We then use that to point our light at the player positioned from above.

10. Wrap up and some nice water!

Go ahead and add this at the end of your createEnvironment method:

        # water
        self.water = self.loader.loadModel('models/square.egg')
        self.water.setSx(self.worldsize*2)
        self.water.setSy(self.worldsize*2)
        self.water.setPos(self.worldsize/2,self.worldsize/2,25) # sea level
        self.water.setTransparency(TransparencyAttrib.MAlpha) 
        newTS = TextureStage('1')
        self.water.setTexture(newTS,self.loader.loadTexture('models/water.png'))
        self.water.setTexScale(newTS,4)
        self.water.reparentTo(self.render)
        LerpTexOffsetInterval(self.water,200,(1,0),(0,0), textureStage=newTS).loop()

You will need one more model and texture, download links are below, place the files in your models directory:

In our usual style, we’re not going to discuss this until the next issue! The only part you may need to tweak for your game is the setPos call. The last parameter (25 in the code above) is your sea-level. Play around with that value to suit your game. On running your game, you should find you have nice animated water! It should also be semi-transparent allowing you to see the terrain beneath the sea. The animation is subtle, so slow down or run in OOBE mode if you cannot observe the motion in the ocean!

Motion in the Ocean

Now fly into the sea. Notice you do not crash! Go back to your setupCollisions routine and add one more bitmask:

        self.water.setCollideMask(BitMask32.bit(0))

Problem solved! Until next time, goodbye! There is one more page of this Issue, an Appendix detailing the full game code as it currently stands. In Issue 5 we will be adding a heads up display (HUD) and some sound while refactoring our code to support the games growth.