We will try to make a clone of the game Asteroids that has been released in 1979.
Our version will look like this:
The project is quite complex. It uses a few things that were not covered by the beginner's course. I know that you will be able to look them up.
If you go through the project alone, it is possible that you get stuck at some problem. If that happens to you, let us know. We will be happy to help you!
The first step is to program a spaceship that you can control by keyboard.
Spaceship
represents the spaceship.x
and y
(position),
x_speed
and y_speed
, rotation
, and
sprite
. (A sprite is a 2D object in Pyglet with position, speed, rotation, and image.)tick
that handles the spaceship mechanics – movement,
rotation, and control.objects
.
It should contain only the spaceship for now.set
).
It is a datatype similar to list but without order. It can contain each element
only once. (Sets are like dictionaries without values.)
You can use the sets cheatsheet,
and the official Python documentation contains
a tutorial
and a reference.
The spaceship uses the set as part of the processing in its tick
method.In the game, we will use a large number of Sprite
s. Drawing them one by one would take quite a long time.
So add all the Sprite
s to the (pyglet.graphics.Batch)[https://pythonhosted.org/pyglet/api/pyglet.graphics.Batch-class.html] collection, which Pyglet can efficiently draw at once. Add arguments to "batch" by using Sprite()
to create a sprite.delete()
. For example:
batch = pyglet.graphics.Batch()
sprite1 = pyglet.sprite.Sprite(image, batch=batch)
sprite2 = pyglet.sprite.Sprite(image, batch=batch)
# and then you can draw all of them at once:
batch.draw()
Create the batch
collection, as well as the objects
, as global variables.
image = pyglet.image.load(...)
image.anchor_x = image.width // 2
image.anchor_y = image.height // 2
self.sprite = pyglet.sprite.Sprite(image, batch=batch)
You can use the arrow keys to move the rocket left, right, and straight. The arrows to the sides spin the rocket, the arrow forward accelerates the movement in the direction the rocket is turned.
self.x = self.x + dt * self.x_speed
self.y = self.y + dt * self.y_speed
self.rotation = self.rotation + dt * rotation_speed
ROTATION_SPEED = 4 # radians per second
self.x_speed += dt * ACCELERATION * math.cos(self.rotation)
self.y_speed += dt * ACCELERATION * math.sin(self.rotation)
If you have calculated the self.x
, self.y
, and self.rotation
values, do not forget
to project them into self.sprite
, otherwise nothing interesting will happen.
Beware that the math.sin
and math.cos
functions use radians, whereas the pyglet
Sprite.rotation
uses degrees. (Additionally, they start at different origins, and they rotate
in opposite directions.) For a sprite, therefore, the angle needs to be converted:
self.sprite.rotation = 90 - math.degrees(self.rotation)
self.sprite.x = self.x
self.sprite.y = self.y
Spaceship
object maintains its own state, so it should not be difficult
to create more (and to control all at once).Bonus 2 : You may have noticed a "jump" when a rocket escapes from the window and returns to the other side. This can be avoided by rendering the whole screen once more to the left, right, up and down.
Pyglet has a special low-level feature that can tell "now draw everything moved by the X pixels to the left". Full explanation would be long, so just copy the code:
from pyglet import gl
def draw():
window.clear()
for x_offset in (-window.width, 0, window.width):
for y_offset in (-window.height, 0, window.height):
# Remember the current state
gl.glPushMatrix ()
# Move everything drawn from now on by (x_offset, y_offset, 0)
gl.glTranslatef(x_offset, y_offset, 0)
# Draw
batch.draw()
# Restore remembered state (this cancels the glTranslatef)
gl.glPopMatrix()
For an overview, the documentation for the functions used here is: glPushMatrix, glPopMatrix, glTranslatef.
Have you succeeded? Can you fly through the universe?
Add a second type of space object: Asteroid
.
SpaceObject
class, in which will be
everything they have in common, and a Spaceship
class, that inherits from SpaceObject
, in which the spaceship-specific
code remains (i.e., keyboard control, ship image, start from the middle of the screen).super()
function (more in inheritance lesson).Asteroid
class, which is also inherited from SpaceObject
, but has its own
behaviour: it starts either at the left or bottom of the screen with a random speed, and each
asteroid has a randomly assigned image. (In the Asteroids, the left and right edges
are essentially the same, and the top and bottom too.)Have you succeeded? Do you have two types of objects?
Our asteroids are still pretty harmless. Let's change that.
radius
attribute.In order to see what the game "thinks" where and how big our objects are,
draw a circle around each object. The best thing to do is to use
pyglet.gl
and a little math; for now, just copy the draw_circle
function and call it for each object.
After you got this part working, you won't need to highlight the radius any longer,
and you can remove the draw_circle function again.
def draw_circle(x, y, radius):
iterations = 20
s = math.sin(2 * math.pi / iterations)
c = math.cos(2 * math.pi / iterations)
dx, dy = radius, 0
gl.glBegin(gl.GL_LINE_STRIP)
for i in range(iterations + 1):
gl.glVertex2f(x + dx, y + dy)
dx, dy = (dx * c - dy * s), (dy * c + dx * s)
gl.glEnd()
SpaceObject.delete
method, because any object can be removed from the game. In this
method, you must remove the object from the list of objects
and then delete its Sprite
so that it does not render within the batch
.And how do you detect that collision? Within the Spaceship.tick
, go through each object to
see if the distance between the ship and the other object is less than the sum of their radiuses
(they hit each other), and if so, call the object's hit_by_spaceship
method.
Finding a distance in a game where the objects that fly out of the screen return on the other side is not entirely straightforward, so copy the code for now:
def distance(a, b, wrap_size):
"""Distance in one direction (x or y)"""
result = abs(a - b)
if result > wrap_size / 2:
result = wrap_size - result
return result
def overlaps(a, b):
"""Returns true if and only if two objects overlap space"""
distance_squared = (distance(a.x, b.x, window.width) ** 2 +
distance(a.s, b.y, window.height) ** 2)
max_distance_squared = (a.radius +b.radius) ** 2
return distance_squared < max_distance_squared
Most other objects in the completed game (such as fire from the rocket, missile) will not do anything
when the collision happens, so the SpaceObject.hit_by_spaceship
should do nothing
(the method only needs to exist). Only an asteroid will break the rocket, so redefine Asteroid.hit_by_spaceship
to call delete
ship.
Because there could be more rockets in our game in general, the asteroid needs to know which rocket it broke.
The hit_by_spaceship
method should, therefore, have an argument.
Have you succeeded? Can you lose now?
Now try to break the asteroids.
tick
method. If the number is negative user can fire again.Laser
. The laser starts at the
rocket's coordinates, it has the rocket's rotation and rocket speed plus something extra in the
direction of rotation.Laser
object needs to "remember" how long it is in the game. In the beginning,
set its lifetime to a number so the laser can fly little bit further than one screen. When its lifetime
is over, the Laser
disappears.tick
method, the laser goes through all objects, and when its position overlaps with some
of these objects, it calls their hit_by_laser
method. For most objects, this method does nothing,
only the asteroids will break.When the laser touches an asteroid, the asteroid divides into two smaller ones (or, if it's too small, it disappears completely).
You can set the speeds of new asteroids how you want - it is important that every smaller asteroid flies elsewhere. Usually, new asteroids are faster than the original ones.
And that's all! You have a functional game!
Have you succeeded? Can you also win?
If you want to continue in the game, here are some ideas. You can do it in any order - or you can invent your own extension!
Is the game too difficult?
You can add lives: there are three at the beginning, and as long as there's one left,
the rocket will appear again in the middle of the screen with zero speed after an
asteroid hit it.
The game should also ignore the keys that were held until the player presses them again
(preferably use pressed_keys.clear ()
).
You can show the number of ships (lives) that are left with icons at the bottom of the screen.
Bonus: A few seconds after the "restart", the rocket can be indestructible to have time to fly when there happens to be an asteroid in the middle of the screen.
Is the game too easy?
Add Levels: When the player shoots all the asteroids, they move to the next level where there are more the asteroids than in the previous level.
You can display the level number using pyglet.text.Label.
Is the background too black?
In the set of pictures in the Backgrounds
directory choose one background and paint
the whole universe with it.
Is the game too austere?
Add fire and explosions! Like the Laser
, only they don't destroy anything, they just
change their colour depending on how long they are in the game.
You can use the "Smoke particle assets"
images drawn by Kenney Vleugels again. I recommend "White Puff".
You can shrink them (e.g. sprite.scale = 1/10
), change their colour (e.g. sprite.color = 255, 100, 0
),
or make them partially transparent (e.g. sprite.opacity = 100
).
I recommend to make a new batch
for the effects and draw them before the main batch, so the
effects can't overlap the game objects.
Don't you know whether you lost or won?
In the end, you can draw a big GAME OVER or WINNER sign.
Are you bored?
In the original game, UFOs sometimes appear, and sometimes they shoot at the rocket,
so if the rocket stands still in one spot and it is just spinning around, the UFO will
destroy it. You can try to complete the Ufo
class and you can create ShipLaser
and
UfoLaser
that inherit from the Laser
class.
Have you succeeded? Does it look and behave professionally?