Gyula Rabai

Zombie game

Written by: Gyula Rabai

Zombie game is a simple top-down survival shooter game built with Python and RGame. This game demonstrates the capabilities of the RGame game engine. It uses traditional controls (WASD) to move the main character areound and the mouse to use the weapon. The source files show how easy and efficient it is to use RGame.

Download: ZombieGame.zip

Figure 1 - Screenshot

Game Overview

Survive as long as possible against waves of zombies. Shoot zombies, collect better guns, and try to reach higher levels. Each level spawns more zombies and they move faster.

How to Run the Game

Make sure you have Python installed, then simply run:

python MainScript.py

Controls

  • Move: W / A / S / D
  • Shoot: Left Mouse Button (hold for rapid fire)
  • Pick up better gun: Touch the grey rectangle (gun pickup) that spawns after each level

Gameplay Mechanics

Weapons & Upgrades

  • You start with a basic pistol.
  • After each level, a better gun spawns randomly on the map (grey rectangle).
  • Touch the gun with your player to upgrade.
  • Upgrades increase: Rate of Fire, Magazine size, Reload speed (some weapons)
  • At level 5+, you may get a shotgun (fires spread of pellets).

Zombies

  • Spawn at the left edge of the screen.
  • Chase the player directly.
  • More zombies and faster speed each level.
  • Collision with player = Game Over.

Levels

  • Each level lasts 10 seconds or until all zombies are killed.
  • After level ends a new gun spawns, zombies get faster.
  • Level number shown in top-left corner.

Game Over

Touching a zombie ends the game with a big red "Game Over!" message.

Technical Information

Main Files

  • MainScript.py: Core game logic, player, levels, UI
  • Zombie.py: Zombie AI and behavior
  • Bullet.py: Bullet logic (collision, optional auto-aim)
  • Gun.py: Base gun + Revolver + Shotgun implementations
  • RGameLib.py: Custom game engine / rendering library

Key Features in Code

  • Custom 2D vector math (RG_Vector2D, RG_Velocity2D)
  • Simple physics system with deltaTime
  • Object pooling not used: objects are created/deactivated
  • Basic collision detection (circle-circle)

Tips for Better Performance

  • Collect every gun upgrade: especially rate of fire and magazine size.
  • Keep moving: standing still makes you easy to hit.
  • Shotgun (level 5+) is great for crowds but has slow reload.
  • Try to clear zombies quickly to get more time before the next wave.

Source code

MainScript.py

from Zombie import*
from Bullet import*
from Gun import*
from RGameLib import*

# This is the rest of the code
# Do not worry if you do not understand any of it
# We will cover this later.

class Player(RG_Script):
    
    def Start(self):
        self.Speed = 7.5
        self.MainWindow.Bind("w", self.Up)
        self.MainWindow.Bind("<KeyRelease-w>", self.UpR)
        self.MainWindow.Bind("s", self.Down)
        self.MainWindow.Bind("<KeyRelease-s>", self.DownR)
        self.MainWindow.Bind("d", self.Right)
        self.MainWindow.Bind("<KeyRelease-d>", self.RightR)
        self.MainWindow.Bind("a", self.Left)
        self.MainWindow.Bind("<KeyRelease-a>", self.LeftR)
        self.up = False
        self.down = False
        self.right = False
        self.left = False
        self.gun = Gun(self.MainScript,appearance=RG_App_Circle(self.MainWindow.Screen))
        self.gun.Position = self.Position
        self.gun.rateOfFire = 1
        self.gun.reloadSpeed = 2
        self.gun.magSize = 2
        self.gun.ammo = RandomInt(0, 2)
        pass
    
    def Up(self, args):
        self.up = True
        
    def UpR(self, args):
        self.up = False
        
    def Down(self, args):
        self.down = True
        
    def DownR(self, args):
        self.down = False
        
    def Right(self, args):
        self.right = True
        
    def RightR(self, args):
        self.right = False
        
    def Left(self, args):
        self.left = True
        
    def LeftR(self, args):
        self.left = False
        
    def PhysicsTick(self, deltaTime):
        self.Bounce()
        if(self.MainScript.GameOverTxt.Activated): 
            self.Velocity.SetSpeed(0)
            return
        if(self.up):
            self.Velocity.Y = 1
        if(self.down):
            self.Velocity.Y = -1
        if not (self.up or self.down):
            self.Velocity.Y = 0
        if(self.right):
            self.Velocity.X = 1
        if(self.left):
            self.Velocity.X = -1
        if not (self.left or self.right):
            self.Velocity.X = 0
        if(self.Velocity.GetlengthSqr() != 0):
            self.Velocity.SetSpeed(self.Speed)
        
        if(self.MainWindow.Mouse.Left):
            self.gun.Shoot(self.MainWindow.Mouse.Position)
        if self.MainScript.gun is None:
            return
        if self.MainScript.gun.Position.VectorTo(self.Position).GetLength() < 25 and not (self.MainScript.gun is self.gun):
            self.gun.Deactivate()
            self.gun = self.MainScript.gun
            self.gun.Position = self.Position
        
    def Render(self):
        if self.gun.Appearance.Visible:
            self.gun.Appearance.Visible = False
        pass
        
    pass



        
        
        

def PlayerSetUp(self):
        
    # Making an appearance (a circle)
    playerApp = RG_App_Circle(
            self.MainWindow.Screen,
            radius = 25,
            color = "light yellow",
            outlineWidth = 1,
            offset = RG_Vector2D(25,25)
            )
        
    # Making a position (the centre of the screen)
    playerPos = RG_Position2D(
            self.MainWindow.WindowWidth/2,
            self.MainWindow.WindowHeight/2)
        
    # Creating the actual player with the position and appearance
    self.Player = Player(self, 
            position = playerPos, 
            appearance = playerApp)
        
def TextsSetUp(self):
        
    self.LvlTxtPos = RG_Position2D(335, self.MainWindow.WindowHeight - 10)
    self.LvlTxt = RG_Label(self, self.LvlTxtPos, fontSize=23, text = "Level 1")

    # Same thing with the number of ammo remaining
    self.AmmoTxtPos = RG_Position2D(335, self.MainWindow.WindowHeight-52)
    self.AmmoTxt = RG_Label(self, self.AmmoTxtPos, fontSize=17, text = "Ammo: 1")

    # Time left before next level
    self.TimeTxtPos = RG_Position2D(335, self.MainWindow.WindowHeight - 35)
    self.TimeTxt = RG_Label(self, self.TimeTxtPos, fontSize=17, text = "0")
    
    # Rate of Fire
    self.FireRatePos = RG_Position2D(485, self.MainWindow.WindowHeight - 52)
    self.FireRate = RG_Label(self, self.FireRatePos, fontSize=17, text = "1")

    # The Game Over text
    self.GameOverTxtPos = RG_Position2D(self.MainWindow.WindowWidth/2-100, self.MainWindow.WindowHeight/2)
    self.GameOverTxt = RG_Label(self, self.GameOverTxtPos , fontSize=50, text = "Game Over!", fontColor="Red")
    self.GameOverTxt.Deactivate()

def CreateZombie(self: RG_MainScript):
    height = RandomInt(0,self.MainWindow.WindowHeight)
    zombPos = RG_Position2D(0, height)
    zombApp = RG_App_Circle(self.MainWindow.Screen,
                            radius = 25,
                            color = "green",
                            outlineWidth = 2,
                            offset = RG_Vector2D(25,25))
    zomb = Zombie(self,
                    position = zombPos,
                    appearance = zombApp
                        )
    zomb.Speed = self.ZombieSpeed
    self.Zombies.append(zomb)
    
def DataInit(self):
    self.level = 1 
    self.TimePoint = RG_TimePoint() # The timer used to measure the time elapsed since the level started

    self.spawned = False # weather the zombies have been spawned for the level
    self.Zombies = [] # A list to store the zombies in

    self.Bullets = []# A list to store the bullets in
    self.ZombieSpeed = 1 
    self.gun = None
    
def CreateGun(self, position):
    self.oldGun = self.Player.gun
    if self.level < 5:
        self.gun = Gun(self, position = position, appearance=RG_App_Rectangle(self.MainWindow.Screen, RG_Size(20,10),color="Grey"))
    else:
        self.gun = Gun(self, position = position, appearance=RG_App_Rectangle(self.MainWindow.Screen, RG_Size(50,10),color="Grey"))

# Saying that this bit of code and memory is called MainScript
# and also that MainScript is a type of 'RG_MainScript' meaning it
# can be run



# The first thing that happens in the program
def Main(self):
    # Setting up window
    self.MainWindow.WindowTitle = "The Zombie Game"
    self.MainWindow.WindowBackground = "light green"
    
    PlayerSetUp(self) # Making a player
    TextsSetUp(self) # Making all the texts
    
    # Making variables
    DataInit(self)
    self.levelDuration = 10 # The time in seconds for how long a level lasts
    
    

    
def PhysicsTick(self, deltaTime):
    
    # If zombies have not been spawned spawn them
    if not self.spawned:
        
        # Do this code for how many numbers there are between 0 and 'self.level':
        # I.e. Do this 'self.level' times. I.e. create that many zombies
        for x in range(self.level):
            CreateZombie(self) # Creating a zombie
        
        self.spawned = True
        self.level += 1

def Render(self):
    
    # check if we need to move onto the next level
    NoZombiesLeft = len(self.Zombies) == 0
    NoTimeLeft = self.TimePoint.Diff() >= self.levelDuration
    
    if NoTimeLeft or NoZombiesLeft:
        # Do stuff to set up next level
        self.LvlTxt.Appearance.Text = "Level " + str(self.level)
        self.spawned = False
        self.TimePoint.Now() 
        if self.level > 0:
            if (not (self.gun is self.Player.gun)) and self.gun != None:
                self.gun.Deactivate()
            SpawnGun(self, RG_Position2D(
                RandomInt(50,self.MainWindow.WindowWidth),
                RandomInt(0,self.MainWindow.WindowHeight)))
        self.ZombieSpeed += 0.3

    # Display the amount of ammo remaining
    self.AmmoTxt.Appearance.Text = "Ammo: " + str(self.Player.gun.ammo) + f"/{self.Player.gun.magSize}"
    self.FireRate.Appearance.Text = "Fire Rate: " + str(self.Player.gun.rateOfFire)
    
    # Display the time left before next level.
    timeLeft = self.levelDuration - self.TimePoint.Diff()
    self.TimeTxt.Appearance.Text =  "Time Remaining: "+ format(timeLeft, ".2f")


def SpawnGun(self, position):
    CreateGun(self, position)
    self.gun.rateOfFire = self.oldGun.rateOfFire + 1
    self.gun.magSize = self.oldGun.magSize + 1
    self.gun.ammo = RandomInt(0,self.gun.magSize)
    self.gun.reloadSpeed = 2
    pass

Run(RG_MainScript(None, Main, PhysicsTick, Render))

Zombie.py

from RGameLib import*

class Zombie(RG_Script):
    
    def Start(self):
        self.Speed = 3
        pass
    
    def PhysicsTick(self, deltaTime):
        if(self.MainScript.GameOverTxt.Activated): 
            self.Velocity.SetSpeed(0)
            return
        self.Velocity = self.Position.VectorTo(self.MainScript.Player.Position)
        self.Velocity = self.Velocity.ToVelocity()
        
        if not (self.Velocity.GetLength() < self.Appearance.Dimensions.Radius + self.MainScript.Player.Appearance.Dimensions.Radius):
            self.Velocity.SetSpeed(self.Speed)
        else:
            self.Velocity.SetSpeed(0)
            self.Position = self.MainScript.Player.Position
            self.MainScript.GameOverTxt.Activate()
        pass
        
    def Render(self):
        pass
        
    pass

Gun.py

from RGameLib import *
from Bullet import *

class Gun(RG_Script):

    ROFTimer = RG_TimePoint()
    reloadTimer = RG_TimePoint()
    startedReload = False
    
    rateOfFire = 2
    reloadSpeed = 1
    magSize = 3
    ammo = 3
    bulletSpeed = 10
    
    aimAssist = 2
    
    def PhysicsTick(self, deltaTime):
        
        if(self.ammo == 0 and not self.startedReload):
            self.startedReload = True
            self.reloadTimer.Now()
            
        if(self.reloadSpeed > self.reloadTimer.Diff() and self.startedReload):
            return
        
        if(self.startedReload):
            self.ammo = self.magSize
            self.startedReload = False
        
    
    def Shoot(self, targetPosition):
        
        if(self.reloadSpeed > self.reloadTimer.Diff() and self.startedReload):
            return
        
        if self.ammo == 0: 
            return
        if self.ROFTimer.Diff() < (1/self.rateOfFire): return
        
        self.ammo -= 1
        self.Pos = RG_Position2D(self.Position.X, self.Position.Y)
        self.Vel = self.Position.VectorTo(targetPosition).ToVelocity()
        self.Vel.SetSpeed(self.bulletSpeed)
        self.MainScript.Bullets.append(
            Bullet(self.MainScript,
                    position = self.Pos,
                    velocity = self.Vel,
                    appearance = RG_App_Circle(self.MainWindow.Screen, radius = 2, color = "grey")))
        
        self.ROFTimer.Now()

class Revolver(Gun):
    
    def Start(self):
        self.reloadSpeed = 2
        self.rateOfFire = 3
        
class Shotgun(Gun):
    
    def Start(self):
        self.reloadSpeed = 4
        self.rateOfFire = 0.5
        
    def Shoot(self, targetPosition):
        if(self.reloadSpeed > self.reloadTimer.Diff() and self.startedReload):
            return
        
        if self.ammo == 0: 
            return
        if self.ROFTimer.Diff() < (1/self.rateOfFire): return
        
        self.ammo -= 1
        self.Pos = RG_Position2D(self.Position.X, self.Position.Y)
        self.Vel = self.Position.VectorTo(targetPosition).ToVelocity()
        self.Vel.SetSpeed(self.bulletSpeed)
        for i in range(5):
            randVel = RG_Velocity2D(RandomFloat()*6, RandomFloat()*6)
            self.Vel += randVel
            self.MainScript.Bullets.append(
                Bullet(self.MainScript,
                        position = self.Pos,
                        velocity = self.Vel,
                        appearance = RG_App_Circle(self.MainWindow.Screen, radius = 1, color = "grey")))
            self.MainScript.Bullets[-1].aim = 1
        
        self.ROFTimer.Now()

Bullet.py

from Zombie import*
from RGameLib import*

class Bullet(RG_Script):
    
    aim = 5
    
    def Start(self):
        self.Speed = 10
        pass
    
    def PhysicsTick(self, deltaTime): 
        self.Velocity.SetSpeed(self.Speed)
        zombs = self.MainScript.Zombies
        minZ = None
        if len(zombs) != 0:
            minZ = zombs[0]
        minD = 10000
        for Zomb in zombs:
            if self.Position.VectorTo(Zomb.Position).GetLength() < minD:
                minD = self.Position.VectorTo(Zomb.Position).GetLength()
                minZ = Zomb
            if(self.Position.VectorTo(Zomb.Position).GetLength() < Zomb.Appearance.Dimensions.Radius):
                Zomb.Deactivate()
                self.MainScript.Zombies.remove(Zomb)
                self.Deactivate()
                self.MainScript.Bullets.remove(self)
                return
        if(self.Bounce()):
            self.Deactivate()
            self.MainScript.Bullets.remove(self)
        # Uncomment for auto-aiming bullets.
        #if len(zombs) != 0:
        #    toZomb = self.Position.VectorTo(minZ.Position)
        #    toZomb.SetLength(self.aim)
        #    self.Velocity += toZomb * deltaTime
        pass
        
    def Render(self):
        pass
        
    pass

More information


Projects | Books | Printouts | On-line lectures | Presentations