Loading and displaying an image
Our images will be stored in a directory other than the root directory. hence, we must tell pyglet where to look for them: Here is the code for the same.
import pyglet
pyglet.resource.path = ['../resources']
pyglet.resource.reindex()
Now that pyglet's resource module has been initialised, we can easily load images using the resource module's image() function:
player_image = pyglet.resource.image("player.png")
bullet_image = pyglet.resource.image("bullet.png")
asteroid_image = pyglet.resource.image("asteroid.png")
Centering the images
By default, Pyglet will draw and position all images from the lower left corner. This is not what we want for our images, which must rotate around their centres. We only need to set their anchor points to accomplish this. Let's make a function to make this easier:
def center_image(image):
"""Sets an anchor to the image pointing to its center"""
image.anchor_x = image.width // 2
image.anchor_y = image.height // 2
Now we can just call center_image() on all of our loaded images:
center_image(player_image)
center_image(bullet_image)
center_image(asteroid_image)
Remember that the function center_image() must be defined before it can be called at the module level. Also, because zero degrees in pyglet points directly to the right, the images are all drawn with their fronts pointing to the right.
Object initialization
We'd like to add some labels to the top of the window to inform the players of their current score and difficulty level. Eventually, we'll have a score display, the level name, and a row of icons representing the number of lives left.
Creating the labels
Simply initialise a pyglet.text.Label to create a text label in pyglet.
score_label = pyglet.text.Label(text="Score: 0", x=10, y=460)
level_label = pyglet.text.Label(text="My Amazing Game",
x=game_window.width//2, y=game_window.height//2, anchor_x='center')
Drawing the labels
We want the pyglet to execute some code when the window is drawn. An on draw() event is sent to the window to allow it to redraw its contents. pyglet provides several methods for attaching event handlers to objects, one of which is to use a decorator:
@game_window.event
def on_draw():
# draw things here
The @game_window.event decorator informs the Window instance that our on_draw() function handles events. The on_draw() event is triggered whenever the window needs to be redrawn.
We can now populate the method with the functions required to draw our labels. We should clear the screen before drawing anything. After that, we can simply call on_draw() function on each object:
@game_window.event
def on_draw():
game_window.clear()
level_label.draw()
score_label.draw()
Making the player and asteroid sprites
The player should be a pyglet.sprite.sprite instance or subclass, as follows:
from game import resources
...
player_ship = pyglet.sprite.Sprite(img=resources.player_image, x=400, y=300)
Add the following line to on_draw() function to get the player to draw on the screen:
@game_window.event
def on_draw():
...
player_ship.draw()
Loading the asteroids is a little more difficult because we'll need to place more than one at random locations that don't collide with the player right away. Place the loading code in a new game submodule called load.py:
import pyglet
import random
from . import resources
def asteroids(num_asteroids):
asteroids = []
for i in range(num_asteroids):
asteroid_x = random.randint(0, 800)
asteroid_y = random.randint(0, 600)
new_asteroid = pyglet.sprite.Sprite(img=resources.asteroid_image,x=asteroid_x, y=asteroid_y)
new_asteroid.rotation = random.randint(0, 360)
asteroids.append(new_asteroid)
return asteroids
An asteroid could be laid at the player at random and will kill them instantly. To fix this problem, we'll need to know how far away new asteroids are from the player. Here's a quick formula for calculating that distance:
import math
...
def distance(point_1=(0, 0), point_2=(0, 0)):
"""Returns the distance between two points"""
return math.sqrt((point_1[0] - point_2[0]) ** 2 + (point_1[1] - point_2[1]) ** 2)
To compare new asteroids to the player's position, we must pass the player's position into the asteroids() function and keep generating new coordinates until the asteroid is sufficiently distant.
def asteroids(num_asteroids, player_position):
asteroids = []
for i in range(num_asteroids):
asteroid_x, asteroid_y = player_position
while distance((asteroid_x, asteroid_y), player_position) < 100:
asteroid_x = random.randint(0, 800)
asteroid_y = random.randint(0, 600)
new_asteroid = pyglet.sprite.Sprite(
img=resources.asteroid_image, x=asteroid_x, y=asteroid_y)
new_asteroid.rotation = random.randint(0, 360)
asteroids.append(new_asteroid)
return asteroids
Now you can load three asteroids like this:
from game import resources, load
...
asteroids = load.asteroids(3, player_ship.position)
A list of sprites has been added to the asteroids variable. Drawing them on the screen is as easy as drawing the player's ship - simply call their on_draw() methods:
@game_window.event
def on_draw():
...
for asteroid in asteroids:
asteroid.draw()
Basic motion
The second version of the example will include a simpler, faster method for drawing all of the game objects, as well as a row of icons indicating the number of lives remaining.
Drawing with batches
When there are many different types of objects, manually calling each object's draw() method can become cumbersome and tedious. It's also inefficient if you need to draw a lot of objects. pyglet pyglet.graphics The Batch class makes drawing easier by allowing you to draw all of your objects with a single function call. Simply create a batch, pass it into each object you want to draw, and invoke the draw() method of the batch.
simply create an instance of pyglet.graphics.Batch to create a new batch:
main_batches = pyglet.graphics.Batch()
Simply add the batch keyword argument to an object's function to make it a member of a batch:
score = pyglet.text.Label(text="Score: 0", x=20, y=575, batch=main_batch)
Displaying little ship icons
We'll need to draw a small row of icons in the upper right corner of the screen to show how many lives the player has left. Because we'll be making more than one using the same template, let's create a function in the load module called player_lives() to generate them. The icons should be identical to the player's ship. We could use an image editor to create a scaled version, or we could just let pyglet do the scaling. I'm not sure about you, but I prefer the option that requires the least amount of effort.
The function for creating icons is nearly identical to the one for creating asteroids.
def player_lives(num_icons, batch=None):
player_lives = []
for i in range(num_icons):
new_sprite = pyglet.sprite.Sprite(img=resources.player_image,
x=785-i*30, y=585, batch=batch)
new_sprite.scale = 0.5
player_lives.append(new_sprite)
return player_lives
The player icon is 50x50 pixels in size, so half that size is 25x25. We want to leave some space between each icon, so we make them at 30-pixel intervals from the right side of the screen to the left.
Making things move
If nothing on the screen ever moved, the game would be extremely boring. We'll need to create our own set of classes to handle frame-by-frame movement calculations in order to achieve motion. We'll also need to create a Player class to respond to keyboard input.
Creating the basic motion class
Because every visible object has at least one Sprite, we can make our basic motion class a subclass of pyglet.sprite.Sprite. Another option is to give our class a sprite attribute.
Declare a PhysicalObject class in a new game submodule called physicalobject.py. The only new attributes we'll add will be to store the object's velocity, so the function will be straightforward:
class PhysicalObject(pyglet.sprite.Sprite):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.vel_x, self.vel_y = 0.0, 0.0
Each object will need to be updated frame by frame, so let's write an update() method:
def update(self, dt):
self.x += self.vel_x * dt
self.y += self.vel_y * dt
Writing the game update function
To call the update() method of each object every frame, we must first have a list of those objects. For the time being, we can just declare it after we've set up all the other objects:
game_objects = [player_rocket] + asteroids
Now we can use a simple function to loop over the array:
def update(dx):
for obj in game_objects:
obj.update(dx)
The update() function takes a dx parameter because it is still not the source of the actual time step.
Calling the update() function
The objects must be updated at least once per frame. What exactly is a frame? Most monitors have a maximum refresh rate of 60 hertz. However, if we set our loop to run at exactly 60 hertz, the motion will appear jerky because it will not exactly match the screen. To achieve smooth animation, we can have it update twice as fast, at 120 times per second.
The most effective way to call a function 120 times per second is to use a pyglet. The pyglet.clock module provides a number of methods for calling functions on a regular basis or at a future time. The one we're looking for is pyglet.clock.schedule interval():
pyglet.clock.schedule_interval(update, 1/120.0)
This line, placed above pyglet.app.run() in the if __name__ == '__main__' block, instructs Pyglet to call update() 120 times per second. Pyglet will use the elapsed time, dt, as the sole parameter.
Writing class of the Player
The player object must respond to keyboard input in addition to obeying the basic laws of physics. Create a game.player module, import the necessary modules, and subclass PhysicalObject:
from . import physical object, resources
class Player(physicalobject.PhysicalObject):
def __init__(self, *args, **kwargs):
super().__init__(img=resources.player_img, *args, **kwargs)
The only difference between a Player and a PhysicalObject thus far is that a Player always has the same image. However, Player objects require a few more attributes. Because the ship will always thrust in the same direction, we'll need to define a constant for the magnitude of that force. We should also define a constant for the rotation speed of the ship:
self.thrust = 300.0
self.rotate_speed = 200.0
Now we need to get the class to respond to user input.
self.keys = dict(left=False, right=False, up=False)
Then we must implement two methods: on_key_release() and on_key_press(). When pyglet examines a new event handler, it searches for the following two methods, among others:
import math
from pyglet.window import key
from . import physicalobject, resources
class Player(physicalobject.PhysicalObject)
def on_key_press(self, symbol, modifiers):
if symbol == key.UP:
self.keys['up'] = True
elif symbol == key.LEFT:
self.keys['left'] = True
elif symbol == key.RIGHT:
self.keys['right'] = True
def on_key_release(self, symbol, modifiers):
if symbol == key.UP:
self.keys['up'] = False
elif symbol == key.LEFT:
self.keys['left'] = False
elif symbol == key.RIGHT:
self.keys['right'] = False
That appears to be quite time-consuming. There is a better way, which we will see later, but for now, this version serves as a good demonstration of pyglet's event system.
The final step is to write the update() method. It behaves similarly to a PhysicalObject with a few extras, so we'll need to call the PhysicalObject's update() method and then respond to input:
def update(self, dt):
super(Player, self).update(dt)
if self.keys['left']:
self.rotation -= self.rotate_speed * dt
if self.keys['right']:
self.rotation += self.rotate_speed * dt
So far, so simple. To rotate the player, we simply multiply the rotation speed by the angle, then multiply by dt to account for time. The rotation attributes of Sprite objects are in degrees, with clockwise as the positive direction.
if self.keys['up']:
angle_radians = -math.radians(self.rotation)
force_x = math.cos(angle_radians) * self.thrust * dt
force_y = math.sin(angle_radians) * self.thrust * dt
self.velocity_x += force_x
self.velocity_y += force_y
First, we convert the angle to radians so that the values returned by math.cos() and math.sin() are correct. Then we use some basic physics to change the ship's X and Y velocity components and steer the ship in the right direction.
Integrating the player class
The first step is to make player ship an instance of Player:
from game import player
...
player_rocket = player.Player(x=500, y=400, batch=main_batch)
Now we need to tell pyglet that player_rocket is an event handler. To do that, we need to push it onto the event stack with game_window.push_handlers():
game_window.push_handlers(player_rocket)
Giving the Player something to do
Moving up to the next in depth game example in pyglet, let's see how something must be working against the Player in any good game. It's the risk of colliding with, well, an asteroid in the case of Asteroids. Collision detection necessitates a substantial amount of infrastructure in the code, so this section will concentrate on making it work. We'll also clean up the player class and show some thrusting visual feedback.
Simplifying player input
The Player class currently handles all of its own keyboard events. It only uses 13 lines of code to set boolean values in a dictionary. There has to be a better way, right? There is: pyglet.window.key.KeyStateHandler. This useful class does what we have been doing manually: it tracks the state of each key on the keyboard.
We need to change the update() method to use the key handler because Player now relies on it to read the keyboard. The only difference is in the if conditions:
if self.key_handler[key.LEFT]:
...
if self.key_handler[key.RIGHT]:
...
if self.key_handler[key.UP]:
...
Adding an engine flame
It's tough to determine if the ship is thrusting forward or not without visual feedback, especially if you're just watching someone else play the game. Showing an engine flame behind the Player when thrusting is one technique to convey visual feedback.
Loading the flame image
Two sprites will now make up the Player. Nothing prevents a Sprite from owning another Sprite, so we'll simply give Player an engine sprite attribute and update it every frame. This is the simplest and most scalable technique for our needs.
We could either conduct some difficult arithmetic every frame or simply move the image's anchor point to have the flame drawn in the correct spot. To begin, open resources.py and load the image:
engine_image = pyglet.resource.image("engine_flame_x.png")
Creating and drawing the flame
The engine sprite requires the same arguments as Player, with the exception that it requires a separate image and must be initially invisible. The code for generating it is simple and can be found in Player. init__():
self.engine_sprite = pyglet.sprite.Sprite(img=resources.engine_image, *args, **kwargs)
self.engine_sprite.visible = False
Cleaning up after death
The Player will vanish from the screen after being blasted to bits by an asteroid. However, removing the Player instance from the game objects list alone will not remove it from the graphics batch. To do so, we must use the delete() method. Normally, a Sprite's own delete() method would suffice, but our subclass has its own child Sprite (the engine flame), which must be erased as well when the Player instance is deleted. We need to develop a simple but significantly updated delete() method to get both to expire gracefully:
def delete(self):
self.engine_sprite.delete()
super(Player, self).delete()
Checking For collisions
We'll need to alter the game objects list to make objects vanish from the screen. Every object must compare its position to that of every other object, and each object must decide whether or not it should be removed from the list. The game loop will then look for and delete any dead objects from the list.
Implementing the collision functions
The PhysicalObject class requires three additions: the dead attribute, the collides_with() method, and the handle collision_with() method. Because the collides_with() method will need to use the distance() function, let's start by moving it into its own game submodule called util.py:
import pyglet, math
def distance(point_1=(0, 0), point_2=(0, 0)):
return math.sqrt(
(point_1[0] - point_2[0]) ** 2 +
(point_1[1] - point_2[1]) ** 2)
In load.py, remember to call from util import distance. We can now write PhysicalObject.collides_with() without having to duplicate code:
def collides_with(self, other_object):
collision_distance = self.image.width/2 + other_object.image.width/2
actual_distance = util.distance(self.position, other_object.position)
return (actual_distance <= collision_distance)
The collision handler function is even simpler because for now, we just want every object to die when it comes into contact with another object:
def handle_collision_with(self, other_object):
self.dead = True
And there you have it! You should be able to zip around the screen with your engine blazing. If you collide with something, both you and the object you collided with should vanish from the screen. There is still no game, but we are clearly progressing.
Collision response
Bullets will be added to this area. This new feature will necessitate starting to add items to the game objects list throughout the game, as well as having objects check each other's kinds to determine whether or not they should die.
Adding objects during play
A boolean flag was used to handle object removal. Adding items will be a little more complicated. For one reason, an object cannot simply request to be added to a list. It must originate from somewhere. Furthermore, an object may wish to add more than one additional object at the moment.
There are a few options for resolving this issue. We'll make each object hold a list of new child objects to being added to game objects to avoid circular references, keep our constructors concise, and avoid introducing unneeded modules. This method will simplify any game object to spawn other things.
Adding bullets
Bullets behave similarly to other physical objects for the most part, but they have two distinct characteristics, at least in this game: they only collide with certain objects, and they vanish from the screen after a few seconds to prevent the Player from flooding the screen with bullets.
First, create a new game submodule named bullet.py and create a simple PhysicalObject subclass:
import pyglet
from . import physical object, resources
class Bullet(physicalobject.PhysicalObject):
"""Bullets fired by the player"""
def __init__(self, *args, **kwargs):
super(Bullet, self).__init__(
resources.bullet_image, *args, **kwargs)
Firing bullets
Let's open up the Player class, import the bullet module, and add a bullet speed attribute to its function Object()
...
from . import bullet
class Player(physicalobject.PhysicalObject):
def __init__(self, *args, **kwargs):
super(Player, self).__init__(img=resources.player_image, *args, **kwargs)
...
self.bullet_speed = 700.0
We can now write the code to generate a new bullet and launch it into space. The on keypress() event handler must first be resurrected:
def on_key_press(self, symbol, modifiers):
if symbol == key.SPACE:
self.fire()
The fire() method will be a little more involved. The majority of the calculations will be relatively similar to those for thrusting, with a few exceptions. We'll need to launch the bullet from the ship's nose, not its middle.
Customizing collision behavior
There are five types of collisions in our current version of the game: bullet-asteroid, bullet-player, asteroid-player, bullet-bullet, and asteroid-asteroid. In a more complex game, there would be many more.
We may generalize this behavior in PhysicalObject because objects of the same type should not be destroyed when they collide. Other exchanges will necessitate some further effort.
Letting twins ignore each other
We only need to verify if two asteroids or two bullets' classes are equal in the PhysicalObject.handle a collision with a method to allow them to pass each other by without a word of acknowledgment (or a dramatic explosion):
def handle_collision_with(self, other_object):
if other_object.__class__ == self.__class__:
self.dead = False
else:
self.dead = True
Making asteroids explode
Asteroids are challenging for players because each asteroid you shoot converts into other asteroids. If we want our game to be enjoyable, we must imitate that behavior. We've already completed the majority of the difficult tasks. Now we have left to create a new PhysicalObject subclass and implement a special handle collision with the () method, as well as a few maintenance modifications.
Writing the asteroid class
Make a new game submodule called asteroid.py. Write the standard function to pass a specific image to the superclass, as well as any other parameters:
import pyglet
from . import resources, physicalobject
class Asteroid(physicalobject.PhysicalObject):
def __init__(self, *args, **kwargs):
super(Asteroid, self).__init__(resources.asteroid_image, *args, **kwargs)
We must now create a new handle_collision_with() method. It should generate a random number of smaller asteroids with random speeds. However, it should only do so if it is sufficiently large. An asteroid should only divide twice, and if we scale it down by half each time, it should stop dividing when it's 1/4 the size of a new asteroid.
import random
class Asteroid:
def handle_collision_with(self, other_object):
super(Asteroid, self).handle_collision_with(other_object)
if self.dead and self.scale > 0.25:
num_asteroids = random.randint(2, 3)
for i in range(num_asteroids):
new_asteroid = Asteroid(x=self.x, y=self.y, batch=self.batch)
new_asteroid.rotation = random.randint(0, 360)
new_asteroid.velocity_x = (random.random() * 70 + self.velocity_x)
new_asteroid.velocity_y = (random.random() * 70 + self.velocity_y)
new_asteroid.scale = self.scale * 0.5
self.new_objects.append(new_asteroid)
Frequently Asked Questions
How do I know if pyglet is installed?
Show activity on this post. If you have pip installed, it should be easy. Just use "pip install pyglet."
What is the use of pyglet in Python?
Pyglet is a library for the Python programming language that provides an object-oriented application programming interface to create games and other multimedia applications.
How to start with Visual Studio Code to create the asteroid game?
Start by creating a new folder in the SRC file in Visual Studio Code itself. The code will include importing pyglet libraries and making a game window.
What all characters and features can we add to the asteroid game?
The player controls a solitary spaceship in a space rock field intermittently crossed by flying saucers. The game's object is to shoot and eradicate the space rocks and saucers while not crashing into either.
Conclusion
In this article, we have extensively discussed the concepts of In-depth game examples in Pyglet. We introduced in-depth game examples in Pyglet, Basic graphics of in-depth game examples, and basic motion of the in-depth game example, and in the end, we then concluded with collision response in Pyglet.
We hope that this blog has helped you enhance your knowledge regarding In-depth game examples in pyglet and if you would like to learn more, check out our article on the popular python libraries.
For peeps out there who want to learn more about Data Structures, Algorithms, Power programming, JavaScript, or any other upskilling, please refer to guided paths on Coding Ninjas Studio. Enroll in our courses, go for mock tests, solve problems, and interview puzzles. Also, you can put your attention towards interview stuff- interview experiences and an interview bundle for placement preparations.
Do upvote our blog to help other ninjas grow.
Happy Coding!