Code360 powered by Coding Ninjas X Naukri.com. Code360 powered by Coding Ninjas X Naukri.com
Table of contents
1.
📜Introduction📜
2.
📜Basic Graphics📜
2.1.
💡💡Setting Up
2.2.
💡Getting a Window
2.3.
💡Loading and Displaying the Images
2.4.
💡Centering the Images
2.5.
💡Initializing Objects
2.6.
💡Making the Labels
2.7.
💡Drawing the Labels
2.8.
💡Making the Player and Asteroids Sprites
3.
📜Basic Motion📜
3.1.
🚀Drawing with Batches🚀
3.2.
🚀Displaying Little Ship Icons🚀
3.3.
🚀Making Things Move🚀
3.3.1.
🚀🚀Create Basic Motion Class
3.4.
🚀Writing Game Updation Function🚀
3.5.
🚀Calling The Update Function🚀
3.6.
🚀Writing the Player Class🚀
3.7.
🚀Integrating the Player Class🚀
4.
📜Giving the Player Something to Do📜
4.1.
🔖🔖Simplifying Player Input
4.2.
Adding the Image Flame🔖🔖
4.3.
🔖🔖Loading the Flame Image
4.4.
Creating and Drawing the Flame🔖🔖
4.5.
🔖🔖Cleaning up After Death
4.6.
Checking for collisions🔖🔖
4.7.
🔖🔖Checking all Object Pairs
4.8.
Implementing the Collision Functions🔖🔖
5.
📜Collision Response📜
5.1.
🎯Adding Objects During Play🎯
5.2.
🎯Tweaking the Game Loop🎯
5.3.
🎯Putting the Attribute in PhysicalObject🎯
5.4.
🎯Adding Bullets🎯
5.5.
🎯Firing Bullets🎯
5.6.
🎯Customizing Collision Behavior🎯
5.7.
🎯Modifying the Way Bullets Collide🎯
5.8.
🎯Causing Asteroids to Erupt🎯
5.9.
🎯Asteroid Class in Writing🎯
6.
Frequently Asked Questions
6.1.
How are pyglet buttons created?
6.2.
Is pyglet a game engine?
6.3.
Does Pyglet use OpenGL?
6.4.
What does Pyglet serve?
6.5.
Why is pyglet used?
7.
Conclusion
Last Updated: Mar 27, 2024
Medium

Advanced Topics in Pyglet

Author Mayank Goyal
0 upvote
Master Python: Predicting weather forecasts
Speaker
Ashwin Goyal
Product Manager @

📜Introduction📜

You can follow the instructions in this article to write a straightforward Asteroids clone. The reader is presumed to be experienced with creating and using Python applications. 

pyglet

The player's ship, three asteroids placed at random, and a score of zero will all be displayed in the game's initial iteration. There will be no motion.

📜Basic Graphics📜

Graphics

💡💡Setting Up

To begin with, confirm that Pyglet is installed. The folder structure for our project will then be established. We will have numerous version folders at different phases of development because this example game is written in stages. Outside the sample folders, we will also have a common resources folder called "resources" that contains the photos. A Python code called asteroid.py that plays the game is included in each version folder, along with a subfolder called game, where we will store new modules; here is where the majority of the logic will be. 

💡Getting a Window

Simply import Pyglet and start a new Pyglet. Window instance to create a window. Calling pyglet.app.run() in a window.

import pyglet
game_window = pyglet.window.Window(800, 600)-
if __name__ == '__main__':
    pyglet.app.run()


If we run the code mentioned above, you should see a window filled with garbage that closes when you press Esc.

💡Loading and Displaying the Images

We must instruct Pyglet where to look for our photos because they will be located in a different directory than the example's root directory:

import pyglet
pyglet.resource.path = ['../resources']
pyglet.resource.reindex()


Finding and loading game resources, including graphics, sounds, and other media, is made simple with the pyglet.resource module. All you have to do is reindex it and instruct it where to look. Because the resources folder is on the same level as the version1 folder in this example game, the resource path begins with../. If we didn't do anything, Pyglet would search for the resources/ folder in version1/.

With the resource module of pyglet now initialized, we can quickly load the photos using the 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 positions and draws all pictures from the lower left corner. Our photos must revolve around their centers. Thus we don't want this behavior. Setting their anchor points is all that is required to do this. Let's automate this by writing a function:

def center_image(image):
    "Sets an image's anchor point 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)


Just keep in mind that calling the centre image() method from the module level requires that it first be defined. Additionally, remember that in Pyglet, zero degrees is immediate to the right. Therefore, all the images are drawn with their fronts looking in that direction.

💡Initializing Objects

We want to add some labels to the top of the window to tell the user about their score and the current difficulty level. We'll eventually include a score display, the level's name, and a row of symbols showing how many lives are left.

💡Making the Labels

To make a text label in pyglet, just initialize a pyglet.text.Label object:

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 Pyglet to execute a specific piece of code each time the window is displayed. To allow the window to redraw its contents, an on-draw () event is sent to it. Pyglet offers a variety of methods for affixing event handlers to objects; one straightforward method is to use a decorator:

@game_window.event
def on_draw():
    # draw things here


The @game window informs the Window instance. event decorator that our on draw() function is an event handler. Every time the window has to be redrawn, you guessed it: the on draw() event gets dispatched. On mouse press() and On Key Press are additional events ().

We can now add the functions required to draw our labels to the method. Clearing the screen is necessary before drawing anything. After that, we can just use the draw() function on each object:

@game_window.event
def on_draw():
    game_window.clear()


    level_label.draw()
    score_label.draw()


When you launch asteroid.py, a window with a score of 0 in the upper left corner and the label "My Amazing Game" should appear at the top of the screen.

💡Making the Player and Asteroids Sprites

The player should be a pyglet.sprite instance or subclass. Sprite, as follows:

from game import resources
player_ship = pyglet.sprite.Sprite(img=resources.player_image, x=400, y=300)


Add a line to on draw to cause the player to draw on the screen ()

@game_window.event
def on_draw():


    ...
    player_ship.draw()


It takes a little more work to load the asteroids because we need to scatter more than one of them over the map, so they don't immediately collide with the player. Let's create a new game submodule named load.py and place the loading code there:

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


Making a few new sprites with random placements is all we're doing here. However, there is still a concern because an asteroid may erratically land where the player is, killing them instantly. We'll need to determine how far new asteroids are from the player to address this problem. Here is a straightforward function to figure out 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)


We need to send the player's position into the asteroids() function and keep generating new coordinates until the asteroid is sufficiently away to check new asteroids against the player's position. Sprites in Pyglet keep track of their position using both x and y attributes and a tuple (Sprite. position) (Sprite.x and Sprite.y). We'll only pass the position tuple into the method to keep our code short:

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


It randomly positions each asteroid until it finds one far from the player, producing the sprite and rotating it. A list is returned when each asteroid has been added.

You can now load three asteroids as follows:

from game import resources, load
...
asteroids = load.asteroids(3, player_ship.position)


A list of sprites is now present in the asteroid variable. Calling their draw() methods will draw them on the screen much like the player's ship did:

@game_window.event
def on_draw():
    ...
    for asteroid in asteroids:
        asteroid.draw()


The first section is finished now. Your "game" doesn't currently do much, but we'll get to it in the following parts. To assess our work and locate a working version, you might want to browse through the examples/game/version1 subdirectory in the pyglet source code.

Get the tech career you deserve, faster!
Connect with our expert counsellors to understand how to hack your way to success
User rating 4.7/5
1:1 doubt support
95% placement record
Akash Pal
Senior Software Engineer
326% Hike After Job Bootcamp
Himanshu Gusain
Programmer Analyst
32 LPA After Job Bootcamp
After Job
Bootcamp

📜Basic Motion📜

basic motion

The second version of the example will have a row of icons displaying the remaining lives and a simpler, quicker method for drawing all of the game components. Additionally, we'll develop some programming to force the player and the asteroids to abide by physics.

🚀Drawing with Batches🚀

The draw() method of each object must be manually invoked, which can be time-consuming and difficult if there are many distinct types of objects. It is also incredibly inefficient if you need to draw a huge number of objects—the visuals on Pyglet. The batch class makes drawing simpler by allowing you to draw all of your objects with a single function call. You must execute the batch's draw() method after creating a batch and passing it into each object you want to draw.

Simply create a new instance of pyglet. graphics.Batch to start a new batch:

main batch = graphics.pyglet.Batch()


Simply supply the batch as the batch keyword argument into an object's function to make it a member of the batch:

score_label = pyglet.text
Label(text="Score: 0", x=10, y=575, batch=main_batch)


Each graphic object produced by asteroid.py should have the batch keyword parameter.

We must feed the batch into the game to use it with the asteroid sprites.

Simply include it as a keyword argument to each new sprite after calling the load.asteroid() function. Add a new function:

def asteroids(num_asteroids, player_position, batch=None):
    ...
    new_asteroid = pyglet.sprite.Sprite(img=resources.asteroid_image,
                                        x=asteroid_x, y=asteroid_y,
                                        batch=batch)


Update the location where it is called, too:

asteroids = load.asteroids(3, player_ship.position, main_batch)
These five lines of draw() calls can now be replaced with only one:
main_batch.draw()


Now, asteroid.py should appear the same when you execute it.

🚀Displaying Little Ship Icons🚀

We'll need to draw a little row of symbols in the top-right corner of the screen to indicate how many lives the player still has. Let's make a player lives() function in the load module to produce them as we'll be producing several ones using the same template. The icons ought should resemble the player's spacecraft. We could use an image editor to scale the version or let Pyglet handle it. I don't know about you, but I favor the one that involves the least amount of effort.

Almost identical to the code used to create asteroids is the one used to create icons. We simply make a sprite for each icon, assign it a position and scale, and

Making icons works almost identically to the process of making asteroids. We simply make a sprite for each icon, assign it a location and scale, then add it to the return list:

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


Half of the player icon, 50x50 pixels, will be 25x25 pixels. Since we want to leave room between each icon, we make them at 30-pixel intervals, working our way from the right side of the screen to the left. Remember that player lives(), like the asteroids() function, accepts a batch argument.

🚀Making Things Move🚀

The game would get rather monotonous if nothing ever moved on the screen. We'll need to create our classes to manage frame-by-frame movement computations to produce motion. Additionally, a Player class must be created to respond to keyboard input.

🚀🚀Create Basic Motion Class

We are making our entire motion class a subclass of pyglet.sprite makes sense because at least one Sprite represents every visible object. Sprite. Another strategy would be to add a sprite attribute to our class.

Declare a PhysicalObject class in the physicalobject.py game submodule. The constructor will be straightforward because the object's velocity will be the only new attribute we add.

Every frame, each object must be updated, thus let's create an update() method:

class PhysicalObject(pyglet.sprite.Sprite):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


        self.velocity_x, self.velocity_y = 0.0, 0.0


The "delta time" or "time step" is what it is. Game frames don't appear instantly, and drawing them doesn't always take the same amount of time. If you've ever attempted to play a modern game on a vintage console, you know how variable frame rates may be. There are several solutions to this issue, but the simplest is simply multiplying all time-sensitive actions by dt. Later, I'll demonstrate how to calculate this amount.

Objects will soon fly off the screen if we give them a velocity and then just let them go. We would prefer it if they simply wrapped around the screen because we are creating an Asteroids clone. Here is a straightforward function that satisfies the requirement:

def check_bounds(self):
    min_x = -self.image.width / 2
    min_y = -self.image.height / 2
    max_x = 800 + self.image.width / 2
    max_y = 600 + self.image.height / 2
    if self.x < min_x:
        self.x = max_x
    elif self.x > max_x:
        self.x = min_x
    if self.y < min_y:
        self.y = max_y
    elif self.y > max_y:
        self.y = min_y


As you can see, it just ensures that things are still visible on the screen, and if they aren't, it shifts them to the other side. Add a call to self to force every PhysicalObject to utilize this behaviour of the update's last check bounds().

Simply import the physical object module and modify the new asteroid =... line to generate a new PhysicalObject rather than a Sprite to make the asteroids use our new motion code. Additionally, you should provide them with a random beginning velocity. This is the updated load. Method called asteroids()

def asteroids(num_asteroids, player_position, batch=None):
    ...
    new_asteroid = physicalobject.PhysicalObject(...)
    new_asteroid.rotation = random.randint(0, 360)
    new_asteroid.velocity_x = random.random()*40
    new_asteroid.velocity_y = random.random()*40
    ...

🚀Writing Game Updation Function🚀

We must first acquire a list of those objects to invoke the update() method for each object once every frame. We can just declare it after creating all the other objects for the time being:

game_objects = [player_ship] + asteroids


We can now create a straightforward function to loop through the list:

def update(dt):
    for obj in game_objects:
        obj.update(dt)


Given that it is still not the source of the real-time step, the update() function requires a dt input.

🚀Calling The Update Function🚀

The items must be updated at least once per frame. Describe a frame. Well, the majority of screens have a 60-hertz maximum refresh rate. However, the motion will appear a little jerky if we set our loop to run at exactly 60 hertz because it won't exactly match the screen. To achieve smooth animation, we can double the update rate to 120 times per second.

Asking Pyglet to perform the task is the best technique to call a function 120 times per second. There are several ways to call functions repeatedly or at a future time specified in the pyglet.clock module. Pyglet.clock.schedule interval() is the one we need:

pyglet.clock.schedule_interval(update, 1/120.0)


This line instructs Pyglet to call update() 120 times every second and is placed above Pyglet.app.run() in the if __name__ == '__main__' block. The sole parameter accepted by Pyglet is the amount of time that has passed or dt.

When you run the program asteroid.py, you should observe your formerly static asteroids floating peacefully across the screen and reappearing on the other side as they veer off the edge.

🚀Writing the Player Class🚀

The player object must respond to keyboard input and follow the fundamental principles of physics. Make a game.player module first, then import the necessary modules and subclass PhysicalObject:

from . import physicalobject, resources
class Player(physicalobject.PhysicalObject):

    def __init__(self, *args, **kwargs):
        super().__init__(img=resources.player_image, *args, **kwargs)


The fact that a Player will always have the same image makes it the only difference so far between a Player and a PhysicalObject. However, Player objects require a few different characteristics. We must establish a constant for the size of that force since the ship will always thrust with the same force in the direction it is pointing. Define a constant for the ship's rotational speed as well:

self.thrust = 300.0
self.rotate_speed = 200.0


The class must now be made to respond to user input. Key press and release events are sent to designated event handlers via Pyglet's event-based approach to input. However, we wish to employ a polling strategy in this case, frequently checking to see if a key is down. Keeping a dictionary of keys up to date is one approach to achieving that. The dictionary needs to be initialized in the constructor first:

self.keys = dict(left=False, right=False, up=False)


Following that, we must create two methods: on key press() and key release (). In addition to other things, Pyglet tests a new event handler for these two methods:

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 somewhat laborious. This version provides a nice example of how Pyglet's event system works, but there is a better way to accomplish it, which we'll see later.

The update() method must be written as our final step. It behaves similarly to a PhysicalObject with a few additional characteristics. Thus we must use the PhysicalObject's update() method before responding 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


Quite basic thus far. Simply adding the rotation speed to the angle and multiplying by dt to account for time will rotate the player. Note that the rotation characteristics of Sprite objects are expressed in degrees, with clockwise being the positive direction. This means that if you use the built-in math functions in Python with the Sprite class, you must call it math. degrees() or math. radians() convert the result to a negative value because those procedures use radians instead of degrees, and their positive direction is counter-clockwise. An illustration of such a conversion is used in the code to cause the ship to go forward:

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


To ensure that math.cos() and math.sin() return the right numbers, we first convert the angle to radians. The ship's X and Y velocity components are then changed using basic physics, which moves the ship in the desired direction.

We currently have a finished Player class. We should be ready to go if we add it to the game and inform Pyglet that it's an event handler.

🚀Integrating the Player Class🚀

First, we must create a Player instance called a player ship.

from game import player
...
player_ship = player.Player(x=400, y=300, batch=main_batch)


We must use game window.push handlers() to push it onto the event stack to accomplish that:

game_window.push_handlers(player_ship)


I'm done now! The player should now be able to move using the arrow keys as you play the game.

📜Giving the Player Something to Do📜

play.

Any good game must have an element that works against the player. The risk of collision with an asteroid in the case of asteroids. This section will concentrate on making collision detection a function because it needs a lot of infrastructure in the code to function. The player class will be cleaned up, and visual feedback for thrusting will be demonstrated.

🔖🔖Simplifying Player Input

Right now, every keyboard event is handled by the Player class. It only uses the first 13 lines of code to set boolean values in a dictionary. There is a better option, as there should be: pyglet.window.key.KeyStateHandler. This helpful class maintains the status of each key on the keyboard, automating what we have been doing manually.

Instead of using the Player class, we must initialize it and push it onto the event stack to use it. Let's first add it to the function constructor of the Player:

self.key_handler = key.KeyStateHandler()


The key handler object must also be pushed onto the event stack. We'll need the player ship object to continue handling key press and release events later, so keep pushing it along with its key handler:

game_window.push_handlers(player_ship.key_handler)


We must modify the update() method to use the key handler because the Player now uses it to read the keyboard. The only modifications are to the if statements:

if self.key_handler[key.LEFT]:
    ...
if self.key_handler[key.RIGHT]:
    ...
if self.key_handler[key.UP]:
    …


The on-key press() and key release() functions can now be eliminated from the class. That's how easy it is. You can look in the API docs under pyglet.window.key for a list of key constants.

Adding the Image Flame🔖🔖

Without visual cues, it might be challenging to determine whether the ship is moving forward or not, especially for someone who is simply observing another player. One option to provide the players with visual feedback is to display an engine flame behind them as they shove.

🔖🔖Loading the Flame Image

Two sprites will now make up the player. We'll give Player an engine sprite attribute and update it each frame, as nothing is stopping us from having a Sprite possess another Sprite. This strategy will be the simplest and most adaptable for our needs.

We could either perform intricate mathematic calculations for each frame, or we could just move the image's anchor point to make the flame draw in the proper location. First, open resources.py and load the image:

engine_image = pyglet.resource.image("engine_flame.png")


The center of rotation of the flame image needs to be shifted to the right, past the edge of the image, for the flame to draw behind the player. We simply set its anchor x and anchor y characteristics to accomplish that:

engine_image.anchor_x = engine_image.width * 1.5
engine_image.anchor_y = engine_image.height / 2


The player class is now able to use the image. After reading this section, experiment with the values for the engine image's anchor point if you're still unclear about anchor points.

Creating and Drawing the Flame🔖🔖

All the same arguments as Player must be used to initialize the engine sprite, but it requires a different image and must start invisible. Its creation is handled by the simple code found in Player.__init__():
self.engine_sprite = pyglet.sprite.Sprite(img=resources.engine_image, *args, **kwargs)
self.engine_sprite.visible = False


We must add some logic to the if self.key handler[key.UP] block in the update() method to limit the appearance of the engine sprite to when the player is thrusting:

if self.key_handler[key.UP]:
    ...
    self.engine_sprite.visible = True
else:
    self.engine_sprite.visible = False


We also need to update the sprite's position and rotation attributes for it to appear at the player's position:

if self.key_handler[key.UP]:
    ...
    self.engine_sprite.rotation = self.rotation
    self.engine_sprite.x = self.x
    self.engine_sprite.y = self.y
    self.engine_sprite.visible = True
else:
    self.engine_sprite.visible = False

🔖🔖Cleaning up After Death

The player will eventually be destroyed by an asteroid and vanish from the screen. However, the Player instance cannot be eliminated from the graphics batch by deleting it from the game objects list. We need to use its delete() method to accomplish that. The remove() method of a Sprite usually functions without change, but in our subclass, the engine flame is a Sprite that must also be erased when the Player instance is deleted. We need to create a straightforward but somewhat improved delete() method to make both of them die peacefully:

def delete(self):
    self.engine_sprite.delete()
    super(Player, self).delete()

Checking for collisions🔖🔖

We'll need to tinker with the game objects list to make things vanish from the screen. Every object will have to compare its position to that of every other object to determine whether or not it should be taken from the list. When an object is dead, the game loop will look for it and remove it from the list.

🔖🔖Checking all Object Pairs

Every object must be compared to every other object. The use of nested loops is the most basic approach. This strategy will be ineffective for many objects but will serve our needs. We can avoid testing the same pair of items twice by using a simple optimization. The setup for the loops, which belongs in the update, is shown below (). It does nothing but iterates through all object pairs:

for i in range(len(game_objects)):
    for j in range(i+1, len(game_objects)):
        obj_1 = game_objects[i]
        obj_2 = game_objects[j]


We'll need a method to determine whether an object has already been killed. We could add it to PhysicalObject immediately, but let's focus on the game loop instead and add the method later. We'll just assume for the time being that every item in game objects has a dead property that is first set to False before the class changes it to True, at which point it will be ignored and finally deleted from the list.

We'll need a method to determine whether an object has already been killed. We could add it to PhysicalObject immediately, but let's focus on the game loop instead and add the method later. We'll just assume for the time being that every item in game objects has a dead property that is first set to False before the class changes it to True, at which point it will be ignored and finally deleted from the list.

if not obj_1.dead and not obj_2.dead:
    if obj_1.collides_with(obj_2):
        obj_1.handle_collision_with(obj_2)
        obj_2.handle_collision_with(obj_1)


We simply need to go through the list and eliminate any dead items at this point.

for to_remove in [obj for obj in game_objects if obj.dead]:
    to_remove.delete()
    game_objects.remove(to_remove)


It just calls the object's delete() method to remove it from any batches, as you can see, and then it removes the object from the list. If you've never worked with list comprehensions before, the code above may appear to eliminate items from the list as it traverses it. Fortunately, the list comprehension is assessed before the execution of the loop. Thus there shouldn't be any issues.

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, so let's start by relocating it into its game submodule called util.py:

import math, pyglet
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, don't forget to call from util import distance. Now, without having to duplicate code, we can write PhysicalObject.collides_with():

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)


Because we only want each item to disappear as soon as it contacts another object, for the time being, the collision handler code is even more straightforward:

def handle_collision_with(self, other_object):
    self.dead = True


One last thing: in PhysicalObject.__init__, set self.dead = False ().

That's all, then! You should be able to move quickly around the screen with your engine blazing. You and the object should vanish off the screen when you collide with something. Although there is still no game, it is evident that we are moving forward.

📜Collision Response📜

collison response.

We'll include bullets in this area. With this new functionality, we'll need to add items to the game objects list as the game progresses and have objects compare their types to determine whether or not they should perish.

🎯Adding Objects During Play🎯

We used a boolean flag to manage object removal. It will be a little trickier to add items. One thing is that an object cannot simply request to be added to a list. It must originate someplace. Another possibility is that an object will wish to combine multiple objects simultaneously.

This issue can be resolved in several ways. We'll have each object maintain a list of new child objects that should be added to game objects to prevent circular references, keep our constructors neat and concise, and prevent extra modules. Any item in the game will find it simple to spawn more things

🎯Tweaking the Game Loop🎯

Add two lines of code to the game objects loop to check objects for children and add those children to the list. The new objects attribute has not yet been implemented, but when it is, it will include a list of additional objects:

for obj in game_objects:
    obj.update(dt)
    game_objects.extend(obj.new_objects)
    obj.new_objects = []


Unfortunately, there are issues with this straightforward fix. Changing a list while iterating over it is a bad idea. The solution is as simple as adding new items to a different list, after which we may iteratively add those items to game_objects.

Just above the loop, declare a to_add list and insert new items into it. Add the items to to_add to game_objects at the very bottom of update(), following the object removal code:

...collision...
to_add = []
for obj in game_objects:
    obj.update(dt)
    to_add.extend(obj.new_objects)
    obj.new_objects = []
...removal...
game_objects.extend(to_add)

🎯Putting the Attribute in PhysicalObject🎯

We only need to define the new objects attribute in the PhysicalObject class, as was already mentioned:

def __init__(self, *args, **kwargs):
    ....
    self.new_objects = []


All we need to do to add a new item and place it in the new_objects list; the main loop will then recognize it, add it to the game_objects list, and clear new_objects.

🎯Adding Bullets🎯

Writing the bullet class.

In this game, at least, there are two exceptions to the general behaviour of bullets: they only collide with certain physical objects, and they vanish from view after a certain period to prevent player oversaturation.

Make a simple subclass of PhysicalObject in a new game submodule named bullet.py first.

import pyglet
from . import physicalobject, resources

class Bullet(physicalobject.PhysicalObject):
    "Bullets fired by the player."

    def __init__(self, *args, **kwargs):
        super(Bullet, self).__init__(
            resources.bullet_image, *args, **kwargs)


We could keep track of our ages and lifespan characteristics to make the bullets eventually vanish, or we could delegate the task to Pyglet. Writing a function to be called at the end of a bullet's life is the first step.

def die(self, dt):
    self.dead = True


We need to instruct Pyglet to make the call in around a half-second. Add a call to pyglet.clock.schedule_once() to the constructor so that it is called as soon as the object is initialized so that we can do this:

def __init__(self, *args, **kwargs):
    super(Bullet, self).__init__(resources.bullet_image, *args, **kwargs)
    pyglet.clock.schedule_once(self.die, 0.5)


The Bullet class still needs work, but let's get them on the screen before we continue working on the class itself.

🎯Firing Bullets🎯

Since the Player class will be the only one that can fire shots, let's open it up, import the bullet module, and give its function Object() { [native code] } a bullet speed attribute:

...
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


Now that the code has been written, a new bullet can be produced and launched into orbit. To begin with, we must revive the on-key press() event handler:

def on_key_press(self, symbol, modifiers):
    if symbol == key.SPACE:
        self.fire()


It will be a little more difficult to use the fire() method itself. There will be some variations, but most of the calculations will be relatively similar to those for thrusting. The ship's nose, not its centre, is where the bullet has to be spawned. If the player travels at a high enough speed, the bullets will travel at a slower speed than the ship. Therefore we'll also need to add the ship's current velocity to the new velocity of the bullet.

Reverse the direction and convert to radians as usual:

def fire(self):
    angle_radians = -math.radians(self.rotation)

ship_radius = self.image.width/2
bullet_x = self.x + math.cos(angle_radians) * ship_radius
bullet_y = self.y + math.sin(angle_radians) * ship_radius
new_bullet = bullet.Bullet(bullet_x, bullet_y, batch=self.batch)

bullet_vx = (
    self.velocity_x +
    math.cos(angle_radians) * self.bullet_speed
)
bullet_vy = (
    self.velocity_y +
    math.sin(angle_radians) * self.bullet_speed
)
new_bullet.velocity_x = bullet_vx
new_bullet.velocity_y = bullet_vy

self.new_objects.append(new_bullet)


You should be able to fire weapons from the front of your ship now. There is only one issue: your ship vanishes as soon as you fire. Asteroids also vanish when they collide, as you may have noticed earlier. We'll need to modify each class's handle_collision_with() method to address this issue.

🎯Customizing Collision Behavior🎯

In the game's current iteration, there are five collision types: bullet-player, bullet-asteroid, bullet-bullet, asteroid-player, and asteroid-asteroid. In a more complicated game, there would be a lot more.

We may generalize that behaviour in PhysicalObject because objects of the same type should typically not be destroyed when they collide. A bit more effort will be needed for other encounters.

Allowing twins to avoid one another

We only need to verify if their classes are equal in the PhysicalObject.handle a collision with a () method to let two asteroids or two bullets pass each other unnoticed or with a dramatic explosion):

def handle_collision_with(self, other_object):
    if other_object.__class__ == self.__class__:
        self.dead = False
    else:
        self.dead = True


Although there are a couple more, more sophisticated techniques to verify object equality in Python, the code above works.

🎯Modifying the Way Bullets Collide🎯

Given how widely different objects' bullet collision behaviour can be, let's add a reacts to bullets attribute to PhysicalObjects that the Bullet class can use to decide whether or not to register a collision. Additionally, we ought to provide the is_bullet attribute to assess the collision between both objects accurately.

(These design choices are not ideal, but they will function.)

First, in the PhysicalObject constructor, initialize the reacts to bullets attribute to True:

class PhysicalObject(pyglet.sprite.Sprite):
    def __init__(self, *args, **kwargs):
        ...
        self.reacts_to_bullets = True
        self.is_bullet = False
        ...
class Bullet(physicalobject.PhysicalObject):
    def __init__(self, *args, **kwargs):
        ...
        self.is_bullet = True


Then add the following code to PhysicalObject.collides_with() to allow bullets to be ignored in certain situations:

def collides_with(self, other_object):
    if not self.reacts_to_bullets and other_object.is_bullet:
        return False
    if self.is_bullet and not other_object.reacts_to_bullets:
        return False
    …


Finally, in Player.__init__(), set self.reacts_to_bullets to False. The entire Bullet class has been completed. Let's now simulate what would occur if a bullet struck an asteroid.

🎯Causing Asteroids to Erupt🎯

Players find Asteroids tough because each asteroid you shoot creates new asteroids. If we want our game to be enjoyable, we must imitate that behaviour. Most of the difficult aspects have already been completed. The only things left to do are to create another subclass of PhysicalObject, create a unique handle_collision_with() method, and make a few maintenance changes.

🎯Asteroid Class in Writing🎯

Make an asteroid.py submodule of the game. To pass a specific image to the superclass and any additional parameters, create the standard constructor as follows:

import pyglet
from . import resources, physicalobject
class Asteroid(physicalobject.PhysicalObject):
    def __init__(self, *args, **kwargs):
        super(Asteroid, self).__init__(resources.asteroid_image, *args, **kwargs)


A new handle_collision_with() method must now be written. It should generate an arbitrary number of new, smaller asteroids with arbitrary speeds. But if it's big enough, it should only be able to accomplish that. If we scale down an asteroid by half each time it divides, it should stop when it is only 1/4 the size of a new asteroid. An asteroid should only divide up to two times.

The function begins with a call to the superclass's method because we want to maintain the previous behavior of disregarding other asteroids:

def handle_collision_with(self, other_object):
    super(Asteroid, self).handle_collision_with(other_object)


Now that we know it's going to die, we can argue that if it's big enough, we should make two or three new asteroids with arbitrary rotations and velocities. To make it appear as though they originated from the same object, we should add the velocity of the old asteroid to the new ones:

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)


While we're here, let's give the asteroids a minor graphical enhancement by making them rotate. We'll do so by including the rotate speed parameter and assigning a random value to it. The update() method will then be created to apply that rotation to every frame.

Constructor: Include the following attribute:

def __init__(self, *args, **kwargs):
    super(Asteroid, self).__init__(resources.asteroid_image, *args, **kwargs)
    self.rotate_speed = random.random() * 100.0 - 50.0


Writing the update() method:

def update(self, dt):
    super(Asteroid, self).update(dt)
    self.rotation += self.rotate_speed * dt


The final step is to navigate to load.py and instruct the asteroid() method to produce a new Asteroid rather than a PhysicalObject:

from . import asteroid
def asteroids(num_asteroids, player_position, batch=None):
    ...
    for i in range(num_asteroids):
        ...
        new_asteroid = asteroid.Asteroid(x=asteroid_x, y=asteroid_y, batch=batch)
        ...
    return asteroids

Frequently Asked Questions

How are pyglet buttons created?

To create a button on a computer screen, draw a rectangle, add some text to it, and then listen to the mouse click to see if the button is clicked on the rectangle.

Is pyglet a game engine?

The Python windowing and multimedia library pyglet are designed for creating visually appealing games and other applications.

Does Pyglet use OpenGL?

An interface to OpenGL and GLU is provided by pyglet. Pyglet's higher-level APIs use the interface so that the graphics card can render everything effectively rather than the operating system. This interface is directly accessible, and using it from C is similar to using OpenGL.

What does Pyglet serve?

The Python programming language's pyglet package offers an object-oriented application programming interface to create games and other multimedia applications. Pythonglet is available under the BSD License and runs on Microsoft Windows, macOS, and Linux.

Why is pyglet used?

A Python multimedia library is called Pyglet. It is employed in the creation of visually appealing games and applications. Both Windows and Linux support it. It also supports the joystick, the user interface, and game control.

Conclusion

This article taught us about the Advanced topic in pyglet to create a game, its different parameters, requirements, and functionalities. That's all from the article. I hope you all like it.

You can refer to pygamepygletpyglet documentationblender 3.4 documentationARDevelop on NVIDIA OmniverseGoogle AR and VRMetaARCoreGoogle Play, and  Microsoft Mesh.

Also, you can check our courses and test series for your interview preparations: Coding Ninja Test SeriesCoding QuestionsInterview Preparation ResourcesCoding ContestsProduct-based Company Course, Data Science & ML courseMaster Data Analytics, and ML for beginners. You can also refer to other IT subjects like compiler design and software engineering.

Happy Coding!

Previous article
Debugging Options in Pyglet
Next article
In-depth game examples in pyglet
Live masterclass