November 24, 2010

ISSUE 3: Lights, Action, Camera!

Welcome back! In this issue you will learn how to add a sky to your game, add some lighting and some fog. You will create controls for your player and be able to fly around your 3D world, barrel roll, loop-the-loop and more! We’ll also dip into how the camera works and how to control it while also explaining how positioning and transformations work. With a further snippet on performance tuning there’s a lot to cover, so away we go…!

1. Position and Transformation

We left Issue 2 with our player positioned on the terrain but did not explain those last few lines of code. So here they are again:

        self.player = self.loader.loadModel("alliedflanker.egg")
        self.player.setPos(20,20,65)
        self.player.setH(225)
        self.player.reparentTo(self.render)

The first line should look familiar. The same way we loaded our world, we load the model for our player. Likewise, the fourth line, re-parents the player to render. Again, exactly as we did with our world in Issue 1.

Those other two lines do warrant some explanation. We first make a call to ‘setPos’ which, you may have guessed, sets the position of our player. Four values are given. The first one is our world. This is an important concept you will see time and time again with Panda3D. We are performing relative positioning. Position our player relative to the world. It’s a powerful approach – your game objects can be positioned, moved and oriented relative to each other.

Having said that, in this instance, you could completely omit the world parameter and just call setPos with the 3 numbers given!  We do not specify the worlds position in our code and thus it defaults to 0,0,0 …making our relative positioning effectively normal positioning. That does not detract from the fact that it’s a very powerful concept, hence our inclusion in the code snippet.

The remaining three values are X, Y and Z. The diagram shown should hopefully be self explanatory, note that where the 3 lines intersect, all values are zero. As such, XYZ values can be positive or negative:

XYZ and HPR Positioning

You can also set these co-ordinates individually instead of all at once as we have done here. There’s a summary table below of what Panda3D API provides.

Next, that other line of code where we called ‘setH’. H is short for Heading (sometimes called Yaw). As well as having a position a model also has a ‘transformation’, again given by three numbers. Heading (H), Pitch (P) and Roll (R). As with position, we can set them individually or all at once. Refer back to the XYZ diagram above where we also marked out HPR and what the values do. We can even set position and transformation at the same time with one call. Below is a summary of the ‘set’ methods available for a models position, transformation and size (scale):

 

ScalePositionTransformation
setScale(sx,sy,sz)
setSx(sx)
setSy(sy)
setSz(sz)
setPos(x,y,z)
setX(x)
setY(y)
setZ(z)
setHpr(h,p,r)
setH(h)
setP(p)
setR(r)
setPosHprScale(x,y,z,h,p,r,sx,sy,sz)
…set all of them in one go!

 

Try changing some of the values and re-run your game. Notice what happens. A large part of game development is trial and error! You should be able to re-position your player and also alter the orientation of your player. Remember, your world is 1024×1024 (refer back to our export settings in Issue 1 from L3DT) with a maximum terrain height (highest mountain peak) set to 60 – so it’s quite easy to position your player off the map, under the ground or hidden in a mountain by accident! Notice we set our initial player position above to 65 – making sure it was never hidden underground irrespective of XY values.

There are some tricks and techniques to positioning that we’ll cover in a future issue along with some other debugging techniques. For now, however, you will have to re-run your game every time you change the values to see what happens. What you are looking to find is your preferred ‘start position’ for the player. Ultimately, we’ll store this value in a ‘constant’ variable, but right now – the values are only used once in the whole code so it is not necessary (wise developers make the correct decision between over and under engineering a solution, they also recognise when it is time to re-engineer).

2. Learning to Fly

Enough is enough! We have tinkered with some cool things in the last two issues but, as of yet, we haven’t got anything playable! Time to change that by making it possible to fly your player around your world. First, update your code to look like the below. Note we have now introduced line numbers – don’t type them in, they’re just there to make it easier for us to refer back to the code!

TOP TIP: There is probably an option in the text editor you are using to either display line numbers alongside your code (exactly as we have done below) or show the line number the cursor is on at the bottom of the screen.

from direct.showbase.ShowBase import ShowBase
from direct.task import Task
import sys

class ShooterGame(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        self.world = self.loader.loadModel("world.bam")
        self.world.reparentTo(self.render)

        # relevant for world boundaries
        self.worldsize = 1024

        self.player = self.loader.loadModel("alliedflanker.egg")
        self.player.setPos(self.world,200,200,65)
        self.player.setH(self.world,225)
        self.player.reparentTo(self.render)

        # A task to run every frame, some keyboard setup and our speed
        self.taskMgr.add(self.updateTask, "update")
        self.keyboardSetup()
        self.speed = 0.0
        self.maxspeed = 100.0
        self.player.setScale(.2,.2,.2)

        # performance (to be masked later by fog) and view:
        self.maxdistance = 400
        self.camLens.setFar(self.maxdistance)
        self.camLens.setFov(60)

    def keyboardSetup(self):
        self.keyMap = {"left":0, "right":0, "climb":0, "fall":0, \
                              "accelerate":0, "decelerate":0, "fire":0}
        self.accept("escape", sys.exit)
        self.accept("a", self.setKey, ["accelerate",1])
        self.accept("a-up", self.setKey, ["accelerate",0])
        self.accept("z", self.setKey, ["decelerate",1])
        self.accept("z-up", self.setKey, ["decelerate",0])
        self.accept("arrow_left", self.setKey, ["left",1])
        self.accept("arrow_left-up", self.setKey, ["left",0])
        self.accept("arrow_right", self.setKey, ["right",1])
        self.accept("arrow_right-up", self.setKey, ["right",0])
        self.accept("arrow_down", self.setKey, ["climb",1])
        self.accept("arrow_down-up", self.setKey, ["climb",0])
        self.accept("arrow_up", self.setKey, ["fall",1])
        self.accept("arrow_up-up", self.setKey, ["fall",0])
        self.accept("space", self.setKey, ["fire",1])
        self.accept("space-up", self.setKey, ["fire",0])
        base.disableMouse() # or updateCamera will fail!

    def setKey(self, key, value):
        self.keyMap[key] = value

    def updateTask(self, task):
        self.updatePlayer()
        self.updateCamera()
        return Task.cont

    def updatePlayer(self):
        # Global Clock
        # by default, panda runs as fast as it can frame to frame
        scalefactor = (globalClock.getDt()*self.speed)
        climbfactor = scalefactor * 0.5
        bankfactor  = scalefactor
        speedfactor = scalefactor * 2.9

        # Climb and Fall
        if (self.keyMap["climb"]!=0 and self.speed > 0.00):
            # faster you go, quicker you climb
            self.player.setZ(self.player.getZ()+climbfactor)
            self.player.setR(self.player.getR()+climbfactor)
            # quickest return: (avoids uncoil/unwind)
            if (self.player.getR() >= 180):
                self.player.setR(-180)
        elif (self.keyMap["fall"]!=0 and self.speed > 0.00):
            self.player.setZ(self.player.getZ()-climbfactor)
            self.player.setR(self.player.getR()-climbfactor)
            # quickest return:
            if (self.player.getR() <= -180):
                self.player.setR(180)
        # autoreturn - add a bit regardless to make sure it happens
        elif (self.player.getR() > 0):
                self.player.setR(self.player.getR()-(climbfactor+0.1))
                if (self.player.getR() < 0):
                    self.player.setR(0) # avoid jitter
        elif (self.player.getR() < 0):
                self.player.setR(self.player.getR()+(climbfactor+0.1))
                if (self.player.getR() > 0):
                    self.player.setR(0)

        # Left and Right
        if (self.keyMap["left"]!=0 and self.speed > 0.0):
            self.player.setH(self.player.getH()+bankfactor)
            self.player.setP(self.player.getP()+bankfactor)
            # quickest return:
            if (self.player.getP() >= 180):
                self.player.setP(-180)
        elif (self.keyMap["right"]!=0 and self.speed > 0.0):
            self.player.setH(self.player.getH()-bankfactor)
            self.player.setP(self.player.getP()-bankfactor)
            if (self.player.getP() <= -180):
                self.player.setP(180)
        # autoreturn
        elif (self.player.getP() > 0):
                self.player.setP(self.player.getP()-(bankfactor+0.1))
                if (self.player.getP() < 0):
                    self.player.setP(0)
        elif (self.player.getP() < 0):
                self.player.setP(self.player.getP()+(bankfactor+0.1))
                if (self.player.getP() > 0):
                    self.player.setP(0)

        # throttle control
        if (self.keyMap["accelerate"]!=0):
            self.speed += 1
            if (self.speed > self.maxspeed):
                self.speed = self.maxspeed
        elif (self.keyMap["decelerate"]!=0):
            self.speed -= 1
            if (self.speed < 0.0):
                self.speed = 0.0

        # move forwards - our X/Y is inverted, see the issue
        self.player.setX(self.player,-speedfactor)

        # respect max camera distance else you
        # cannot see the floor post loop the loop!
        if (self.player.getZ() > self.maxdistance):
            self.player.setZ(self.maxdistance)
        # should never happen once we add collision, but in case:
        elif (self.player.getZ() < 0):
            self.player.setZ(0)

        # and now the X/Y world boundaries:
        if (self.player.getX() < 0):
            self.player.setX(0)
        elif (self.player.getX() > self.worldsize):
            self.player.setX(self.worldsize)

        if (self.player.getY() < 0):
            self.player.setY(0)
        elif (self.player.getY() > self.worldsize):
            self.player.setY(self.worldsize)

    def updateCamera(self):
        # see issue content for how we calculated these:
        self.camera.setPos(self.player, 25.6225, 3.8807, 10.2779)
        self.camera.setHpr(self.player,94.8996,-16.6549,1.55508)

my_shooter_game = ShooterGame()
my_shooter_game.run()

Now run your program. You should find you can now move the player around! Use the ‘A’ key to increase speed, ‘Z’ to decrease. With a little speed, the arrow keys will allow you to turn left/right and allow you to climb/fall in the sky. You can even do a loop-the-loop or barrel roll! You can now also quit the game via the ESC key. If you’re not able to see the terrain, your model is likely positioned too high. Simply accelerate a little and then dive, you should see something!

The model itself is actually turning and twisting as you play yet, you will notice, it looks perfectly stationary on your screen – it appears that the camera is twisting and turning to follow. That’s exactly what the code is doing as we will discuss momentarily. It doesn’t have to work that way, you can do all manner of camera trickery, but we have opted for ‘follow the player’ for now.

Notice also you can fly straight through the terrain! Collision with the terrain is something we have yet to consider.

3. Understanding the Code

The constructor (__init__ method) should look largely familiar, with a few additions. Line 12 stores the world size for reference. Lines 19-24 add a task to the task manager. The Panda3D task manager runs every frame. What we are saying here is to run our method called updateTask every frame. We define updateTask later in our code (lines 54-57). Other changes to our constructor call another method – keyboardSetup – which we define on lines 31 to 49, set an initial speed of zero, a maximum speed of 100 and scale the player model to 1/5 of its original size (this is purely cosmetic).

Perhaps the most important additions here are lines 26-29. When we ran the game on a fairly low end laptop with a baby sized graphics card the performance of the game was not admirable. Even if the game had executed painlessly you should still always consider performance (particularly if you plan on distributing your game where the ‘end-users PC specification’ is a big unknown). There are many approaches to improving performance but the one we opted for here was to call ‘setFar’ on the camera lens and set it to 400. This means that the anything further away than 400 from the camera lens is simply not drawn on the screen. Our world is quite large and this helps a lot (the default lens value is 1000). You probably noticed the game drawing the world ahead of the player as you flew around trying your new controls. Try changing the value of maxdistance and see what happens. We will later blend this ‘cut-off’ into our scene using some fog so it is less obvious to the end-user (player!).

We also change the FOV (field of view) here. This actually has a performance hit as, by default, the FOV is only 40 degrees. We’re widening the lens to 60 degrees as it gives a more ‘arcade flight game’ experience. Again, feel free to play with the values and see what happens!

Moving to line 31, we define our keyboardSetup method. Line 32 creates a variable called self.keyMap. The type of variable being created is called a ‘dictionary’ in Python (also called an ‘associative array’ or a ‘hash’). Very succinctly, our keyMap is a set of name-value pairs. Our names are the game controls – accelerate, decelerate, climb, fall, left, right, fire. The values are all set to zero. We could have setup several variables instead of a dictionary. A variable called ‘accelerate’ set to zero and so on. But, by ‘grouping’ them all in this way, all of our names/values are accessible from the one keyMap variable. To access values from a dictionary you would use code like this:

keyMap['accelerate'] = 0
keyMap['left'] = 1

Following the variable declaration, we have several lines calling ‘self.accept’. These lines are telling Panda3D to accept various key presses. Notice that there are two entries for each key we assign – the key being pressed down, and the key being release (‘up’). In every call to accept we pass 3 parameters: the key press we’re interested in, the method to call if the key press happens, the values to pass to that method when the key press happens. Thus:

self.accept("a", self.setKey, ["accelerate",1])
self.accept("a-up", self.setKey, ["accelerate",0])

….equates to:

  • If the ‘a’ key is pressed, call setKey with “accelerate” and “1”.
  • If the ‘a’ key is released, call setKey with “accelerate” and “0”.

We’re using zero and one here to indicate on or off (true/false). The actual setKey method is only 2 lines long (lines 51 and 52) and simply sets the value in our dictionary. By doing this, our keyMap dictionary contains, at all times, the state of the keyboard as last read. Any keys pressed will be captured as will keys being released. We will read back our keyMap later in the code when deciding how to move (fly) the player.

This brings us neatly now to our updateTask method (lines 54-57). Remember, this method gets called every frame of the game. It makes two calls of its own – one to updatePlayer and one to updateCamera (both of which are methods we provide in our code, see below). The last thing it does is ‘return Task.cont’. Task.cont is a constant we gained from the new import you may have noticed on line 2.

In any Python method, you can end by ‘returning’ a value. It’s a bit confusing in this particular instance as you cannot see updateTask being called – be assured that it is being called, frame by frame, by the task manager. To illustrate what a return value does consider the following snippet:

# simple method
def multiplyTwoNumbers(number1,number2):
    return (number1 * number2)

# calling the method
result = multiplyTwoNumbers(10,5)
# the variable 'result' now contains the value 50

If the updateTask method does not return Task.cont the task manager assumes the task is not longer needed and will stop executing it for further frames.

Next we hit the ‘big method’ – updatePlayer(). We will very likely be rewriting (or ‘refactoring’) this in the future for elegance and reuse but at this stage of play, it works admirably for our purposes. Deep breath…

We begin by declaring 4 variables (scalefactor, climbfactor, bankfactor, speedfactor). Notice that they are all related (climb, bank and speed factor are set relative to scale factor). Notice also that we omitted the “self.” you have been seeing prefixing variable names. This means that instead of adding the variables to the object (class instance) the variables are local variables. A local variable exists for a limited time only. The variables are created when the code is executed in updatePlayer and they exist until the end of the method. Once the method has finished executing the variables are discarded (they no longer exist and are deleted from memory). Deciding when to use local variables as opposed to member variables is a decision you will make often (and will see often in future issues).

What are these variables for? Notice the first:

scalefactor = (globalClock.getDt()*self.speed)

This makes use of the globalClock provided by Panda3D. The ‘getDt’ method returns the time since the last frame was drawn. This is another performance related tweak. We want to make sure that the game runs the same on different machines. By default, Panda3D will run ‘as fast as it can’, so you will get a FPS (frames per second) as high as your PC can handle (you don’t always want it to work this way and it can be changed, but for now, it allows our quick solution for controlling game speed). Thus, if you have a high end PC knocking out hundreds of frames per second, this value will be low, and so ‘scalefactor’ will be low. If you have a low end PC knocking out a few frames per second, this value will be high and so ‘scalefactor’ will be high.

We then use scale factor to calculate our climb, bank and speed factors. We will use these to control how fast the player can climb/fall, turn and move forwards. By tying these values to the world clock, we can be assured that the speed will be consistent whatever frame rate the end user is getting. As always, we encourage you to try playing with/changing the values to see what difference it makes to your game. You may prefer faster or slower responses than we have set.

Lines 67-89 deal with the player climbing/falling in the sky. A new important concept is introduced here – the ‘if statement’. An ‘if statement’ allows for conditionally executed code. ‘If’ a certain condition is matched do ‘some action’, if it is not ‘do some other action’. If we were to rewrite lines 67-89 in English it would look a little like this:

IF the player is trying to climb AND the player has some speed THEN do all of this:
    (**1) Adjust the players Z (height) and R (roll)
    IF the roll has gone beyond 180 degrees THEN
        set the roll to -180 degrees (**2)
OTHERWISE IF the player is trying to fall AND the player has some speed THEN:
    Adjust the players Z and R
    IF the roll has gone beyond -180 degrees THEN:
        set the roll to 180 degrees
OTHERWISE IF the player roll is greater than zero THEN:
    reduce it, avoid jitter
OTHERWISE IF the player roll is less than zero THEN:
    increase it, avoid jitter

Notice as always the indentation. In this case, we’re pushing that a little further by introducing a concept called ‘nesting’. Suppose the player presses the down arrow to climb in our game. The FIRST condition we have given (assuming they have some speed) will match (evaluate as true). Therefore, all of the code from (**1) to (**2) will execute. Note that this includes a further if-statement (the so called ‘nested’ if-statement). This is quite separate from the ‘outer’ statement and is executed independently. In other words, the ‘inner’ if-statement is only executed if the outer if-statement evaluates to true. We indicate this to Python as always by using a further level of indentation. This is quite unique to Python. A lot of programming languages do not enforce indentation instead opting to use control characters to group code. For example, in C++ an if-statement might look like this:

if (myVariable == 10) {
     do this
     do that
} else if {
    ….
} else {
   ….
}

C++ uses braces ( { and } ) to control where code blocks start and end. The upshot to this is that indentation becomes meaningless, the code could be written as:

if (myVariable == 10) { do this do that } else if { …. } else { …. }

…and it would still work. The downside is actually the same – we like the fact that Python forces you to lay out your code a certain way, it makes your code readable and this is important. Remember that at some point someone else may be picking up your code to enhance or change it. Trying to understand someone else’s code is always easier if it is laid out nicely! The other downside is that ‘common programming error 101’ for C++ programmers is a missing brace in the code leading to errors that are hard to find and debug. In Python, it’s obvious, because incorrect indentation jumps out like a sore thumb!

In short, then, we use ‘if’ and ‘elif’ in our code to indicate ‘IF’ and ‘OTHERWISE IF’ (or ‘else if’). Notice in the C++ code above there was a final statement that was simply ‘else’. This is equivalent to saying ‘if nothing else matches, do this…’. We don’t need this in our code at the moment but for reference, in Python, you would use ‘else’ in place of ‘elif’ on your final code block of the if-statement. To simplify futher:

  1. If the player climbs we change their Z and R. If R goes above 180 we set it to -180 which is actually exactly the same thing (but there is of course a reason we’re doing this!).
  2. Else If the player falls we change their Z and R. If R goes below -180 we set it to 180 (same)
  3. Else, ‘return to base’

When the player moves and we turn the model it is important that the model returns back to it’s normal position once the player has stopped moving (i.e., released the key). Try it in your game, we do a similar thing on left/right which is easier to observe. If you fly and bank hard to the left/right then release the key – notice the player ‘auto pilots’ back to the default position. Back to climb/fall: if neither key is pressed, we check the roll – if it’s greater than zero, reduce it. If it’s less than zero, increase it. The further ‘nested-if’ avoids something we call ‘jitter’. Imagine if the roll was very slightly above zero – we reduce it, it is slightly below zero. The next time updatePlayer executes (next frame) the opposite will happen – it’s slightly below zero, we increase it. And thus you can end up with an infinite sequence of slight alterations to the model transformation which would jitter (flicker) on the screen. To avoid this, we simply say “if it’s above zero, reduce it, if it is now below zero, set it to zero” and vice versa.

WAIT A MINUTE!!!
Adjust the roll?

Well spotted if you noticed! Based on our previous diagram you would expect to adjust the model pitch for climb/fall and not the model roll. What’s going on? Time for another deep breath (on the plus side, you’ll get a brief break from the code explanation!)…

4. The Problem with our Model…

You’ll notice that we adjust X to fly forwards when you would expect to adjust Y! Try changing your code to Y instead and you’ll find you have a plane that flies sideways! You’ll also notice we adjust roll when climbing/falling where you would expect pitch…. and pitch for left/right where you would expect roll!

Why is this? It comes down to model orientation. You might argue that we provided you with a broken model in Issue 2. In a sense, you would be right. Our rebuttal however would simply state “assumptions are deadly”. There’s a lesson in this when it comes to programming – especially if you are using other people’s libraries or models!

The problem comes down to how the model is oriented. The image below shows what has happened here:

XYZ and HPR Positioning

So we have to decide what to do, here are our options:

  1. Fix the model. This requires some 3D modelling. We haven’t covered creating 3D models yet so you can either head over to Google or discard this option for now.
  2. Swap the model. You could replace the model with a different one. We have not covered ‘acquiring/converting’ models so we’ll discard this too.
  3. Fix it via code. It can be done, but it’s a bit more advanced than we’d like to cover at this stage.
  4. Live with it. We like the model. So, our X is our Y and our Y is our X. Our pitch and roll are inverted too. We can still proceed though, just remembering to invert conventional logic for this particular model!

ERRATA:
There is a command line tool shipped with Panda3D that allows for rotational and positional transformation. See ‘egg-trans’ here: List of Panda3D Executables.

We’re going with Option 4. We are going to live with it. You are welcome to take whichever option you feel most comfortable with! If you choose to follow us, the ‘cheat sheet’ would be:

normally:
    to point the plane up to the sky or down to the ground - we adjust PITCH (P)
    to rotate the plane model left or right (banking) - we adjust ROLL (R)
    to move forwards or backwards we adjust Y
    to move left or right, we adjust X
with our model:
    to point the plane up to the sky or down to the ground - we adjust ROLL (R)
    to rotate the plane model left or right (banking) - we adjust PITCH (P)
    to move forwards or backwards we adjust X
    to move left or right, we adjust Y
in both cases:
    to point the plane left or right, we adjust HEADING (H or Y)
    to move up or down, we adjust Z

5. Ok, back to the code!

Slight tangent there, but we’re back with the code now. We covered climb and fall already. The next block in our code (lines 91-111) does a very similar thing only for moving left and right. We move the player and adjust the model transformation, this time adjusting the pitch (yes, you would expect it to be the roll here normally, as discussed above!).

Lines 113 to 124 deal with speed and moving the player forwards. If accelerate is pressed, increase the speed, up to but not above the ‘maxspeed’ we defined in our constructor. If decelerate is pressed, decrease speed down to but not below zero. We then move the player forwards by adjusting the X value (again, normally you would adjust Y here, but our model is what it is!).

Lines 126 to 132 define our ceiling and floor (maximum and minimum height). Notice that we relate our maximum height to what we defined as our maxdistance in the constructor when setting the camera lens far value. By using this value here to set the maximum height we avoid the scenario where the player flies so high into the sky that, upon descent, the ground simply cannot be seen (because it is further away than the camera lens far value!).

Finally, lines 134 to 143 deal with out world boundaries. Very simply, we just do not allow our player to fly off the terrain (world). If they go beyond the world edges, they are pulled back. We’ll make ‘the end of the world’ look a bit more interesting at a later stage.

And that ends the updatePlayer method! Phew, wasn’t too bad hopefully! That just leaves the one method – updateCamera. You will see (lines 145 to 148) that the camera position and transformation is set relative to the player. In other words, if we set the camera values as all zero, the camera would be in exactly the same position as the player. That wouldn’t be much use (you wouldn’t see the player) so we pull back a little and move a bit higher. You are probably wondering how we came up with the values you see there. All will be explained, in Issue 4!

Having now been through the code, we have one more stop in this issue: to add some lighting, a sky and some fog.

6. Lighting, Fog and a Sky

Issue 3 has been quite a long ordeal! Well done for hanging in there. We’re going to close by adding some nice cosmetic changes to our code. We are not going to discuss them until Issue 4, you’ve had to take in enough for this issue already! To finish up, however, you will need to download a couple of files. A new model (your skydome) and the texture that is associated with it. The links are below. Save the files in the same directory as your game (which is now getting a little cluttered, but we’ll tidy that up in the near future!):

(you may need to right-click and ‘save-as’ depending on your browser/operating system)

There are different approaches to applying a sky. For now, we have opted for a Sky Dome, but we will discuss Sky Spheres and Sky Cubes (cube maps) in the future. If you are interested in how we built the Sky Dome head over to this post on the Panda3D Forums. Now modify your code as follows. First, add the following right at the start of your code with the other imports:

from pandac.PandaModules import CompassEffect
from panda3d.core import AmbientLight, DirectionalLight, Vec4, Vec3, Fog

Next, add a new method to your class. It doesn’t matter where in your code you position this method (relative to others) other than for readability. We put ours after the keyboard methods:

    def createEnvironment(self):
        # Fog to hide a performance tweak:
        colour = (0.0,0.0,0.0)
        expfog = Fog("scene-wide-fog")
        expfog.setColor(*colour)
        expfog.setExpDensity(0.004)
        render.setFog(expfog)
        base.setBackgroundColor(*colour)

        # Our sky
        skydome = loader.loadModel('sky.egg')
        skydome.setEffect(CompassEffect.make(self.render))
        skydome.setScale(self.maxdistance/2) # bit less than "far"
        skydome.setZ(-65) # sink it
        # NOT render - you'll fly through the sky!:
        skydome.reparentTo(self.camera) 

        # Our lighting
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor(Vec4(.6, .6, .6, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection(Vec3(0,-10,-10))
        directionalLight.setColor(Vec4(1, 1, 1, 1))
        directionalLight.setSpecularColor(Vec4(1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

Lastly, add this line as a new line to your constructor/init method:

self.createEnvironment()

Run your game and observe different levels of lighting as you fly around and the sky! Now, head over to the edges of your world and you’ll see the sky runs out! Um, a slight problem there! We need to tend to the area between the terrain edges and where our sky ends. We can’t leave it empty. So we’ll fix it, next time… 😉

7. Wrap up and Summary

Quite an intense issue but we achieved an awful lot! We have managed to make our game playable by adding keyboard controls, done some performance tweaks, controlled the camera, understood some new concepts and begun enhancing the environment our player occupies. Hope you enjoyed Issue 3 and watch out for Issue 4 soon!

Output from Issue 3

Goto Issue 4 >>>