Search Results

Friday, April 1, 2011

Python in the BGE Part 6

Hey. So here's part 6 of my tutorial on using Python to make a (fairly) simple dungeon-crawling game in the BGE.
In the last part, we explored two different options in movement - the first using the built-in Bullet physics engine, and the latter being our own simple physics that we made in Python using simple ray cast collision checks and moving the object ourselves. While the first method using Bullet might be easier for us, it doesn't allow for the greatest degree of freedom and customization - in a high-quality indie game context, while Bullet might be faster, we'll be able to control exactly what happens and when using Python, and it isn't too hard. If you've downloaded the source code for this one, you can see that I've made a few design changes to the Player - he looks a lot better now, don't you think? Also, you can shoot little white bullets with the X-key, which get destroyed on impact with the wall or on disappearing out of view. It's starting to look more like a game, right? So, let's dig right in to the meat of it - the code. Here's the whole source code for Player (I didn't change the camera code)...

----------------
from bge import logic
from bge import events

import math            @ We'll need math functions like atan2

def Sign(value):
 if value > 0:
  return 1
 elif value < 0:
  return -1
 else:
  return 0
 
def Clamp(value, min, max):
 if value > max:
  return max
 elif value < min:
  return min
 else:
  return value
 
def AxisCheck(mx, my, size = 1.0, prop = 'wall'):

 cont = logic.getCurrentController()
 obj = cont.owner

 pos = obj.position         # Get the Player's current position
 topos = pos.copy()         # Make a copy of the Player's current position
 topos.x += (Sign(mx) * size) + mx     # Offset the copy by the movement value for X-axis
 topos.y += (Sign(my) * size) + my     # And offset the copy by the movement value for Y-axis
 
 return obj.rayCast(topos, pos, 0, prop, 1, 1)  # We may have just collided with something
 
def Player():

 cont = logic.getCurrentController()
 sce = logic.getCurrentScene()
 obj = cont.owner
 
 motion = cont.actuators['Motion']
 
 key = logic.keyboard.events
 kbleft = key[events.LEFTARROWKEY]
 kbright = key[events.RIGHTARROWKEY]
 kbup = key[events.UPARROWKEY]
 kbdown = key[events.DOWNARROWKEY]
 kbshoot = key[events.XKEY]

 def Init():
 
  if not 'init' in obj:
  
   obj['init'] = 1
   
   obj['mx'] = 0.0   # Movement values
   obj['my'] = 0.0
   obj['accel'] = 0.05  # Acceleration rate
   obj['maxspd'] = 0.2  # Maximum speed
   obj['friction'] = 0.7 # Friction
   
   obj['dir'] = 0.0  # Direction the Player is facing
   
   obj['atk'] = 5   # Player's attack value
   obj['shottimer'] = 0.0 # Timer for shots
   
   obj['player'] = 1  # Identifying variable for objects looking for Player objects
 
 def Update():
 
  if kbleft > 0:
   obj['mx'] -= obj['accel']
  elif kbright > 0:
   obj['mx'] += obj['accel']
  else:
   obj['mx'] *= obj['friction']
  
  if kbup > 0:
   obj['my'] += obj['accel']
  elif kbdown > 0:
   obj['my'] -= obj['accel']
  else:
   obj['my'] *= obj['friction']
   
  if kbshoot:
  
   if obj['shottimer'] > .2:
   
    obj['shottimer'] = 0.0
    bullet = sce.addObject('Bullet', obj)
    bullet['spd'] = 1
    bullet['dir'] = obj['dir']
    bullet['type'] = 'Player'
    
  obj['shottimer'] += 1.0 / logic.getLogicTicRate() # Increment the timer
   
  obj['mx'] = Clamp(obj['mx'], -obj['maxspd'], obj['maxspd'])
  obj['my'] = Clamp(obj['my'], -obj['maxspd'], obj['maxspd'])
  
  if AxisCheck(obj['mx'], 0, .5)[0] != None: # We just collided with something
   obj['mx'] = 0 # So stop movement on the X-axis
   
  if AxisCheck(0, obj['my'], .5)[0] != None: # We just collided with something
   obj['my'] = 0  # So stop movement on the Y-axis
  
  if AxisCheck(obj['mx'], obj['my'], .5)[0] != None: # We may collide with something diagonally,
   obj['mx'] = 0         # so stop movement on both axes in that case.
   obj['my'] = 0
  
  motion.dLoc = [obj['mx'], obj['my'], 0.0]
  cont.activate(motion)
 
 def Animate():
  
  if max(abs(obj['my']), abs(obj['mx'])) > .1: # Only rotate if you're moving
  
   ori = obj.orientation.to_euler()  
   
   obj['dir'] = math.atan2(obj['my'], obj['mx']) # Set the direction; the '-math.pi * .5' rotates him to the left
   
   ori.z = obj['dir'] # Get the angle we're moving

   obj.orientation = ori
   
 Init()
 Update()
 Animate()
 
def Bullet():
 
 cont = logic.getCurrentController()
 obj = cont.owner
 sce = logic.getCurrentScene()
 cam = sce.active_camera
 
 def Init():
 
  if not 'init' in obj:
   obj['init'] = 1
   if not 'type' in obj: # These variables usually are set by the object who shoots the bullet
    obj['type'] = None # This is a neutral bullet by default, one that can hit anyone
   if not 'spd' in obj:
    obj['spd'] = 0.0
   if not 'dir' in obj:
    obj['dir'] = 0.0
 
 def Update():

  mx = math.cos(obj['dir']) * obj['spd']
  my = math.sin(obj['dir']) * obj['spd']
  
  if not cam.pointInsideFrustum(obj.position):
   obj.endObject()
  
  if AxisCheck(mx, my, .5)[0] != None:
   obj.endObject()
 
  obj.applyMovement([mx, my, 0.0], 0)  # Move the bullet; this way, we don't have to have a motion actuator.
  # We could have also used the local property above (the zero) to move it, but this way we have control over which
  # direction a bullet goes in
 
 Init()
 Update()
---------------- 
Alright. So, there's a bit of new things that we have added this time around. For one, the Player's code has gotten a bit longer. In addition, we added a new function - Bullet. The Bullet function runs on our bullet objects - it doesn't really do anything particularly special yet except move the bullet itself. So, let's examine this line by line...

First off, we import the math module - this module contains useful mathematical functions (dealing with things like arc tangents) and variables (like pi) that we use, particularly in the Bullet code. Also, we added a slight change to the AxisCheck function to allow for property checking - this way we can check for particular properties in our objects, rather than just checking for 'scenery' type objects every time we need to run it.

After we deal with that, we deal with Player code.

sce = logic.getCurrentScene()
...
... 
kbshoot = key[events.XKEY]

Here, we get the current game scene - we'll need this to add objects in. Also, we add a new key definition for the Shoot key (X key). When we press the X-key, the Player will shoot a bullet.

obj['atk'] = 5   # Player's attack value
obj['dir'] = 0.0  # Direction the Player is facing  
obj['shottimer'] = 0.0 # Timer for shots
obj['player'] = 1  # Identifying variable for objects looking for Player type objects 

These are just some variable declarations - we set up a new atk variable. This will be given to each bullet, so that the Player can shoot bullets that vary in strength depending on pickups, environment, etc. The dir variable tells the game which way the Player is facing, while the shottimer is used to time the bullets' firing  - the reload speed. The last variable simply identifies the Player as a Player-type object - why is this important? Well, if the Enemy is looking for the Player, it can identify him this way (or even other 'good guy' type characters).

if kbshoot:
if obj['shottimer'] > .2:
obj['shottimer'] = 0.0
   bullet = sce.addObject('Bullet', obj)
   bullet['spd'] = 1
   bullet['dir'] = obj['dir']
   bullet['type'] = 'Player'
    
obj['shottimer'] += 1.0 / logic.getLogicTicRate() # Increment the timer
 
This code here is how the Player can shoot - if we detect that the shoot key is pressed and that the shot timer is greater than .2 (the reload rate - can be set as a variable to allow for different weapons, for example), then we add a bullet, set the bullet movement speed and direction, and the type of bullet that it is. The type is somewhat important, as using this, we could have good, bad, and 'neutral' bullets that each can influence game characters differently. For example, the Player and allies can shoot good bullets that only hurt enemies, the enemies can shoot bad bullets that only hurt the Player and allies, and neutral characters like animals or civilians can shoot neutral bullets that can hurt anyone. In other words, the type variable just exists for possibility of expansion on the game concept.

if AxisCheck(obj['mx'], obj['my'], .5)[0] != None: # We may collide with something on a diagonal angle,
obj['mx'] = 0         # so stop movement on both axes in that case.
      obj['my'] = 0
 
We perform another AxisCheck function run to ensure that movement is stopped on the diagonal axes as well (previously it was done on just each primary axis).
 
def Animate():
if max(abs(obj['my']), abs(obj['mx'])) > .1: # Only rotate if you're moving noticeably
  
          ori = obj.orientation.to_euler() 
obj['dir'] = math.atan2(obj['my'], obj['mx']) # Set the direction of Player's facing; the -math.pi * .5 rotates him correctly to the left
          ori.z = obj['dir'] # Get the angle we're moving
obj.orientation = ori # Set the orientation
Init()
 Update()
 Animate()

This block of code isn't actually that complex. In this block, we define a new function, Animate, which will rotate the character for us based on his movement values. If the maximum value of the absolute movement values are large enough - if the Player is moving at a non-negligible speed - then it rotates the Player. In the third line, it gets the Player's orientation and converts it to a Euler angle, sets the Player's dir variable to be the angle of his movement (math.atan2), it sets the Player's rotation to be the orientation on the Z axis. Finally, it executes the Animate function after everything else.

The bullet's code isn't anything new - the only thing that we haven't explored already is a few lines in its Update function.
 def Update():

  mx = math.cos(obj['dir']) * obj['spd']
  my = math.sin(obj['dir']) * obj['spd']
  
  if not cam.pointInsideFrustum(obj.position):
   obj.endObject()
  
  if AxisCheck(mx, my, .5)[0] != None:
   obj.endObject()
 
  obj.applyMovement([mx, my, 0.0], 0)  # Move the bullet; this way, we don't have to have a motion actuator.
  # We could have also used the local property above (the zero) to move it, but this way we have control over which
  # direction a bullet goes in
 
As can be seen, there's not too much new things here. When the Player shoots a bullet, we give it a speed and rotation, remember? That rotation is obtained in the Animate function - it's stored in the dir variable. Having gotten the rotation from the Player, the bullet translates the rotation to an X and Y speed using the math modules cos and sin functions - these functions return the X and Y components (respectively) for the given angle. We multiply these components by the bullet's speed, and that's our movement values.

The fourth / fifth and sixth / seventh lines simply destroy the bullet if either 1) the bullet goes outside of the camera's view frustum, or 2) the bullet runs into a scenery object. We get the camera from the lines at the beginning of the bullet's code. When we get the current scene with logic.getCurrentScene(), we can also see which camera is the active one with the scene's active_camera variable. That camera has a function called pointInsideFrustum() which allows us to test a point (a 3-component tuple or vector) to see if it is within the camera's view frustum - if it's onscreen.

The last line that we haven't seen before is the applyMovement() function - this is a nice, Python alternative to the Motion actuator that we used on the Player object. Rather than passing the movement values to the dLoc variable in a Motion actuator, we can simply pass the values as arguments to the applyMovement function. That's about it. Download the source code for Part 6 of the MazeCrawl example game here. As always, have fun with it!

5 comments:

  1. good post budy.. i am just learning blender scripts..

    ReplyDelete
  2. I just went through the all of these up to this one (6th) and before I even finish I need to tell you that these are great. A fantastic reference/starting point for those trying to begin the game engine. Thank you very much.

    ReplyDelete
  3. The Animate function doesn't work like it's supposed to in my .blend file , but my script works with the .blend file you have just fine. In mine the character just rotates quickly and moves through the walls.

    ReplyDelete
    Replies
    1. Since the script isn't the problem and my blend file works with your script I can only assume that there's something odd about your character setup in your blend file. Ensure that you have set the correct collision bounds for the moving objects. Also, see if there's an issue with the level's collision bounds type. Alternately, you can send me your blend file so I can troubleshoot it.

      Delete
    2. A year late but he has modify the motion property "Loc" check box "L" in the player to false or unchecked

      Delete