Like many people, maybe you wanted to write video games when you first learned to code. But were those games like the games you played? Maybe there was no Python when you started, no Python games available for you to study, and no game engines to speak of. With no real guidance or framework to assist you, the advanced graphics and sound that you experienced in other games may have remained out of reach.
Now, there’s Python, and a host of great Python game engines available. This powerful combination makes crafting great computer games much easier than in the past. In this tutorial, you’ll explore several of these game engines, learning what you need to start crafting your own Python video games!
By the end of this article, you’ll:
- Understand the pros and cons of several popular Python game engines
- See these game engines in action
- Understand how they compare to stand-alone game engines
- Learn about other Python game engines available
To get the most out of this tutorial, you should be well-versed in Python programming, including object-oriented programming. An understanding of basic game concepts is helpful, but not necessary.
Ready to dive in? Click the link below to download the source code for all the games that you’ll be creating:
Python Game Engines Overview
Game engines for Python most often take the form of Python libraries, which can be installed in a variety of ways. Most are available on PyPI and can be installed with pip
. However, a few are available only on GitHub, GitLab, or other code sharing locations, and they may require other installation steps. This article will cover installation methods for all the engines discussed.
Python is a general purpose programming language, and it’s used for a variety of tasks other than writing computer games. In contrast, there are many different stand-alone game engines that are tailored specifically to writing games. Some of these include:
- The Unreal Engine
- Unity
- Godot
These stand-alone game engines differ from Python game engines in several key aspects:
- Language support: Languages like C++, C#, and JavaScript are popular for games written in stand-alone game engines, as the engines themselves are often written in these languages. Very few stand-alone engines support Python.
- Proprietary scripting support: In addition, many stand-alone game engines maintain and support their own scripting languages, which may not resemble Python. For example, Unity uses C# natively, while Unreal works best with C++.
- Platform support: Many modern stand-alone game engines can produce games for a variety of platforms, including mobile and dedicated game systems, with very little effort. In contrast, porting a Python game across various platforms, especially mobile platforms, can be a major undertaking.
- Licensing options: Games written using a stand-alone game engine may have different licensing options and restrictions, based on the engine used.
So why use Python to write games at all? In a word, Python. Using a stand-alone game engine often requires you to learn a new programming or scripting language. Python game engines leverage your existing knowledge of Python, reducing the learning curve and getting you moving forward quickly.
There are many game engines available for the Python environment. The engines that you’ll learn about here all share the following criteria:
- They’re relatively popular engines, or they cover aspects of gaming that aren’t usually covered.
- They’re currently maintained.
- They have good documentation available.
For each engine, you’ll learn about:
- Installation methods
- Basic concepts, as well as assumptions that the engine makes
- Major features and capabilities
- Two game implementations, to allow for comparison
Where appropriate, you should install these game engines in a virtual environment. Full source code for the games in this tutorial is available for download at the link below and will be referenced throughout the article:
With the source code downloaded, you’re ready to begin.
Pygame
When people think of Python game engines, the first thought many have is Pygame. In fact, there’s already a great primer on Pygame available at Real Python.
Written as a replacement for the stalled PySDL library, Pygame wraps and extends the SDL library, which stands for Simple DirectMedia Layer. SDL provides cross-platform access to your system’s underlying multimedia hardware components, such as sound, video, mouse, keyboard, and joystick. The cross-platform nature of both SDL and Pygame means that you can write games and rich multimedia Python programs for every platform that supports them!
Pygame Installation
Pygame is available on PyPI, so after creating and activating a virtual environment, you can install it using the appropriate pip
command:
(venv) $ python -m pip install pygame
Once that’s done, you can verify the installation by running an example that comes with the library:
(venv) $ python -m pygame.examples.aliens
Now that you’ve installed Pygame, you can begin using it right away. If you run into problems during installation, then the Getting Started guide outlines some known issues and possible solutions for all platforms.
Basic Concepts
Pygame is organized into several different modules, which provide abstracted access to your computer graphics, sound, and input hardware. Pygame also defines numerous classes, which encapsulate concepts that aren’t hardware dependent. For example, drawing is done on Surface
objects, whose rectangular limits are defined by their Rect
object.
Every game utilizes a game loop to control game play. This loop iterates constantly as the game progresses. Pygame provides methods and functions to implement a game loop, but it doesn’t provide one automatically. The game author is expected to implement the functionality of a game loop.
Each iteration of the game loop is called a frame. Every frame, the game performs four vital actions:
-
Processing user input. User input in Pygame is handled using an event model. Mouse and keyboard input generate events, which can be read and handled, or ignored as you see fit. Pygame itself doesn’t provide any event handlers.
-
Updating the state of game objects. Game objects can be represented using any Pygame data structure or special Pygame class. Objects such as sprites, images, fonts, and colors can be created and extended in Python to provide as much state information as necessary.
-
Updating the display and audio output. Pygame provides abstract access to display and sound hardware. The
display
,mixer
, andmusic
modules allow game authors flexibility in game design and implementation. -
Maintaining the speed of the game. Pygame’s
time
module allows game authors to control the game speed. By ensuring each frame completes within a specified time limit, game authors can ensure the game runs similarly on different hardware.
You can see these concepts come together in a basic example.
Basic Application
This basic Pygame program draws a few shapes and some text on the screen:
The code for this sample can be found below and in the downloadable materials:
1"""
2Basic "Hello, World!" program in Pygame
3
4This program is designed to demonstrate the basic capabilities
5of Pygame. It will:
6- Create a game window
7- Fill the background with white
8- Draw some basic shapes in different colors
9- Draw some text in a specified size and color
10- Allow you to close the window
11"""
12
13# Import and initialize the pygame library
14import pygame
15
16pygame.init()
17
18# Set the width and height of the output window, in pixels
19WIDTH = 800
20HEIGHT = 600
21
22# Set up the drawing window
23screen = pygame.display.set_mode([WIDTH, HEIGHT])
24
25# Run until the user asks to quit
26running = True
27while running:
28
29 # Did the user click the window close button?
30 for event in pygame.event.get():
31 if event.type == pygame.QUIT:
32 running = False
33
34 # Fill the background with white
35 screen.fill((255, 255, 255))
36
37 # Draw a blue circle with a radius of 50 in the center of the screen
38 pygame.draw.circle(screen, (0, 0, 255), (WIDTH // 2, HEIGHT // 2), 50)
39
40 # Draw a red-outlined square in the top-left corner of the screen
41 red_square = pygame.Rect((50, 50), (100, 100))
42 pygame.draw.rect(screen, (200, 0, 0), red_square, 1)
43
44 # Draw an orange caption along the bottom in 60-point font
45 text_font = pygame.font.SysFont("any_font", 60)
46 text_block = text_font.render(
47 "Hello, World! From Pygame", False, (200, 100, 0)
48 )
49 screen.blit(text_block, (50, HEIGHT - 50))
50
51 # Flip the display
52 pygame.display.flip()
53
54# Done! Time to quit.
55pygame.quit()
Despite its humble aspirations, even this basic Pygame program requires a game loop and event handlers. The game loop begins on line 27 and is controlled by the running
variable. Setting this variable to False
will end the program.
Event handling begins on line 30 with an event loop. Events are retrieved from a queue using pygame.event.get()
and are processed one at a time during every loop iteration. In this case, the only event being handled is the pygame.QUIT
event, which is generated when the user closes the game window. When this event is processed, you set running = False
, which will eventually end the game loop and the program.
Pygame provides various methods for drawing basic shapes, such as circles and rectangles. In this sample, a blue circle is drawn on line 38, and a red square is drawn on lines 41 and 42. Note that drawing a rectangle requires you to create a Rect
object first.
Drawing text on the screen is a little more involved. First, on line 45, you select a font and create a font
object. Using that font on lines 46 to 48, you call the .render()
method. This creates a Surface
object containing the text rendered in the specified font and color. Finally, you copy Surface
to the screen using the .blit()
method on line 49.
The end of the game loop occurs on line 52, when everything that was previously drawn is shown on the display. Without this line, nothing would be displayed.
To run this code, use the following command:
(venv) $ python pygame/pygame_basic.py
You should see a window appear with the image shown above. Congratulations! You just ran your first Pygame program!
Advanced Application
Of course, Pygame was designed to write games in Python. To explore the capabilities and requirements of an actual Pygame game, you’ll examine a game written in Pygame with the following details:
- The player is a single sprite on the screen, controlled by moving the mouse.
- At regular intervals, coins appear on the screen one by one.
- As the player moves over each coin, it disappears and the player is awarded ten points.
- As the game progresses, coins are added more quickly.
- The game ends when there are more than ten coins visible on the screen.
When done, the game will look something like this:
The complete code for this game can be found in the downloaded materials and below:
1"""
2Complete Game in Pygame
3
4This game demonstrates some of the more advanced features of
5Pygame, including:
6- Using sprites to render complex graphics
7- Handling user mouse input
8- Basic sound output
9"""
10
11# Import and initialize the pygame library
12import pygame
13
14# To randomize coin placement
15from random import randint
16
17# To find your assets
18from pathlib import Path
19
20# For type hinting
21from typing import Tuple
22
23# Set the width and height of the output window, in pixels
24WIDTH = 800
25HEIGHT = 600
26
27# How quickly do you generate coins? Time is in milliseconds
28coin_countdown = 2500
29coin_interval = 100
30
31# How many coins can be on the screen before you end?
32COIN_COUNT = 10
33
34# Define the Player sprite
35class Player(pygame.sprite.Sprite):
36 def __init__(self):
37 """Initialize the player sprite"""
38 super(Player, self).__init__()
39
40 # Get the image to draw for the player
41 player_image = str(
42 Path.cwd() / "pygame" / "images" / "alien_green_stand.png"
43 )
44 # Load the image, preserve alpha channel for transparency
45 self.surf = pygame.image.load(player_image).convert_alpha()
46 # Save the rect so you can move it
47 self.rect = self.surf.get_rect()
48
49 def update(self, pos: Tuple):
50 """Update the position of the player
51
52 Arguments:
53 pos {Tuple} -- the (X,Y) position to move the player
54 """
55 self.rect.center = pos
56
57# Define the Coin sprite
58class Coin(pygame.sprite.Sprite):
59 def __init__(self):
60 """Initialize the coin sprite"""
61 super(Coin, self).__init__()
62
63 # Get the image to draw for the coin
64 coin_image = str(Path.cwd() / "pygame" / "images" / "coin_gold.png")
65
66 # Load the image, preserve alpha channel for transparency
67 self.surf = pygame.image.load(coin_image).convert_alpha()
68
69 # The starting position is randomly generated
70 self.rect = self.surf.get_rect(
71 center=(
72 randint(10, WIDTH - 10),
73 randint(10, HEIGHT - 10),
74 )
75 )
76
77# Initialize the Pygame engine
78pygame.init()
79
80# Set up the drawing window
81screen = pygame.display.set_mode(size=[WIDTH, HEIGHT])
82
83# Hide the mouse cursor
84pygame.mouse.set_visible(False)
85
86# Set up the clock for a decent frame rate
87clock = pygame.time.Clock()
88
89# Create a custom event for adding a new coin
90ADDCOIN = pygame.USEREVENT + 1
91pygame.time.set_timer(ADDCOIN, coin_countdown)
92
93# Set up the coin_list
94coin_list = pygame.sprite.Group()
95
96# Initialize the score
97score = 0
98
99# Set up the coin pickup sound
100coin_pickup_sound = pygame.mixer.Sound(
101 str(Path.cwd() / "pygame" / "sounds" / "coin_pickup.wav")
102)
103
104# Create a player sprite and set its initial position
105player = Player()
106player.update(pygame.mouse.get_pos())
107
108# Run until you get to an end condition
109running = True
110while running:
111
112 # Did the user click the window close button?
113 for event in pygame.event.get():
114 if event.type == pygame.QUIT:
115 running = False
116
117 # Should you add a new coin?
118 elif event.type == ADDCOIN:
119 # Create a new coin and add it to the coin_list
120 new_coin = Coin()
121 coin_list.add(new_coin)
122
123 # Speed things up if fewer than three coins are on-screen
124 if len(coin_list) < 3:
125 coin_countdown -= coin_interval
126 # Need to have some interval
127 if coin_countdown < 100:
128 coin_countdown = 100
129
130 # Stop the previous timer by setting the interval to 0
131 pygame.time.set_timer(ADDCOIN, 0)
132
133 # Start a new timer
134 pygame.time.set_timer(ADDCOIN, coin_countdown)
135
136 # Update the player position
137 player.update(pygame.mouse.get_pos())
138
139 # Check if the player has collided with a coin, removing the coin if so
140 coins_collected = pygame.sprite.spritecollide(
141 sprite=player, group=coin_list, dokill=True
142 )
143 for coin in coins_collected:
144 # Each coin is worth 10 points
145 score += 10
146 # Play the coin collected sound
147 coin_pickup_sound.play()
148
149 # Are there too many coins on the screen?
150 if len(coin_list) >= COIN_COUNT:
151 # This counts as an end condition, so you end your game loop
152 running = False
153
154 # To render the screen, first fill the background with pink
155 screen.fill((255, 170, 164))
156
157 # Draw the coins next
158 for coin in coin_list:
159 screen.blit(coin.surf, coin.rect)
160
161 # Then draw the player
162 screen.blit(player.surf, player.rect)
163
164 # Finally, draw the score at the bottom left
165 score_font = pygame.font.SysFont("any_font", 36)
166 score_block = score_font.render(f"Score: {score}", False, (0, 0, 0))
167 screen.blit(score_block, (50, HEIGHT - 50))
168
169 # Flip the display to make everything appear
170 pygame.display.flip()
171
172 # Ensure you maintain a 30 frames per second rate
173 clock.tick(30)
174
175# Done! Print the final score
176print(f"Game over! Final score: {score}")
177
178# Make the mouse visible again
179pygame.mouse.set_visible(True)
180
181# Quit the game
182pygame.quit()
Sprites in Pygame provide some basic functionality, but they’re designed to be subclassed rather than used on their own. Pygame sprites don’t have images associated with them by default, and they can’t be positioned on their own.
To properly draw and manage the player and the coins on-screen, a Player
class is created on lines 35 to 55, and a Coin
class on lines 58 to 75. When each sprite object is created, it first locates and loads the image it’ll display, saving it in self.surf
. The self.rect
property positions and moves the sprite on the screen.
Adding coins to the screen at regular intervals is done with a timer. In Pygame, events are fired whenever a timer expires, and game creators can define their own events as integer constants. The ADDCOIN
event is defined on line 90, and the timer fires the event after coin_countdown
milliseconds on line 91.
Since ADDCOIN
is an event, it needs to be handled in an event loop, which happens on lines 118 to 134. The event creates a new Coin
object and adds it to the existing coin_list
. The number of coins on-screen is checked. If there are fewer than three, then coin_countdown
is reduced. Finally, the previous timer is stopped, and a new one starts.
As the player moves, they collide with coins, collecting them as they do. This removes each collected coin from the coin_list
automatically. This also updates the score and plays a sound.
Player movement occurs on line 137. Collisions with coins on the screen are checked on lines 140 to 142. The dokill=True
parameter removes the coin from the coin_list
automatically. Finally, lines 143 to 147 update the score and play the sound for each coin collected.
The game ends when the user either closes the window, or when there are more than ten coins on the screen. Checking for more than ten coins is done on lines 150 to 152.
Because Pygame sprites have no built-in knowledge of an image, they also don’t know how to draw themselves on the screen. The game author needs to clear the screen, draw all the sprites in the correct order, draw the on-screen score, then .flip()
the display to make everything appear. That all happens on lines 155 to 170.
Pygame is a very powerful and well-established library, but it has its drawbacks. Pygame makes game authors work to get their results. It’s up to the game author to implement basic sprite behavior and implement key game requirements such as game loops and basic event handlers. Next up, you’ll see how other game engines deliver similar results while reducing the amount of work you have to do.
Pygame Zero
There are many things Pygame does well, but others where its age is evident. For game-writing beginners, a better option can be found in Pygame Zero. Designed for education, Pygame Zero is guided by a simple set of principles aimed at being perfect for young and beginning programmers:
- Make it accessible: Everything is designed for beginning programmers.
- Be conservative: Support the common platform and avoid experimental features.
- Just work: Make sure everything works without a lot of fuss.
- Minimize runtime costs: If something might fail, fail early.
- Error clearly: Nothing’s worse than not knowing why something went wrong.
- Document well: A framework is only as good as its docs.
- Minimize breaking changes: Upgrading shouldn’t require rewriting your game.
The documentation for Pygame Zero is very accessible for beginning programmers, and it includes a complete step-by-step tutorial. Further, the Pygame Zero team recognizes that many beginning programmers start coding with Scratch, so they provide a tutorial demonstrating how to migrate a Scratch program to Pygame Zero.
Pygame Zero Installation
Pygame Zero is available on PyPI, and you can install it like any other Python library on Windows, macOS, or Linux:
(venv) $ python -m pip install pgzero
Pygame Zero, as its name suggests, is built on Pygame, so this step also installs Pygame as a dependent library. Pygame Zero is installed by default on the Raspberry Pi platform, on Raspbian Jessie or a later release.
Basic Concepts
Pygame Zero automates many things that programmers have to do manually in Pygame. By default, Pygame Zero provides the game creator:
- A game loop, so there’s no need to write one
- An event model to handle drawing, update, and input handling
- Uniform image, text, and sound handling
- A usable sprite class and useful animation methods for user sprites
Because of these provisions, a basic Pygame Zero program can be very short:
1"""
2Basic "Hello, World!" program in Pygame Zero
3
4This program is designed to demonstrate the basic capabilities
5of Pygame Zero. It will:
6- Create a game window
7- Fill the background with white
8- Draw some basic shapes in different colors
9- Draw some text in a specified size and color
10"""
11
12# Import pgzrun allows the program to run in Python IDLE
13import pgzrun
14
15# Set the width and height of your output window, in pixels
16WIDTH = 800
17HEIGHT = 600
18
19def draw():
20 """Draw is called once per frame to render everything on the screen"""
21
22 # Clear the screen first
23 screen.clear()
24
25 # Set the background color to white
26 screen.fill("white")
27
28 # Draw a blue circle with a radius of 50 in the center of the screen
29 screen.draw.filled_circle(
30 (WIDTH // 2, HEIGHT // 2), 50, "blue"
31 )
32
33 # Draw a red-outlined square in the top-left corner of the screen
34 red_square = Rect((50, 50), (100, 100))
35 screen.draw.rect(red_square, (200, 0, 0))
36
37 # Draw an orange caption along the bottom in 60-point font
38 screen.draw.text(
39 "Hello, World! From Pygame Zero!",
40 (100, HEIGHT - 50),
41 fontsize=60,
42 color="orange",
43 )
44
45# Run the program
46pgzrun.go()
Pygame Zero recognizes that the constants WIDTH
and HEIGHT
on lines 16 and 17 refer to the size of the window and automatically uses those dimensions to create it. Plus, Pygame Zero provides a built-in game loop and calls the draw()
function defined on lines 19 to 43 once per frame to render the screen.
Because Pygame Zero is based on Pygame, some shape drawing code is inherited. You can see the similarities in drawing the circle on line 29 and the square on lines 34 to 35:
However, text drawing is now a single function call on lines 38 to 43, rather than three separate functions.
Pygame Zero also provides basic window-handling code, so you can close the window by clicking the appropriate close button, without requiring an event handler.
You can find code demonstrating some of Pygame Zero’s basic capabilities in the downloadable materials:
Running Pygame Zero programs is done from the command line using the command:
(venv) $ python pygame_zero/pygame_zero_basic.py
Running this command will start your Pygame Zero game. You should see a window appear with basic shapes and your Pygame Zero greeting.
Sprites and Images
Sprites are called Actors in Pygame Zero, and they have a few characteristics which require some explanation:
- Pygame Zero provides the
Actor
class. EachActor
has, at minimum, an image and a position. - All images used in a Pygame Zero program must be located in a subfolder called
./images/
, and be named using lowercase letters, numbers, and underscores only. - Images are referenced using only the base name of the image. For example, if your image is called
alien.png
, you reference it in your program as"alien"
.
Because of these built-in features of Pygame Zero, drawing sprites on the screen requires very little code:
1alien = Actor("alien")
2alien.pos = 100, 56
3
4WIDTH = 500
5HEIGHT = alien.height + 20
6
7def draw():
8 screen.clear()
9 alien.draw()
Now you’ll break this small sample down line by line:
- Line 1 creates the new
Actor
object, giving it the name of the image to draw. - Line 2 sets the initial x and y position of the
Actor
. - Lines 4 and 5 set the size of the Pygame Zero window. Notice that
HEIGHT
is based on the.height
attribute of the sprite. This value comes from the height of the image used to create the sprite. - Line 9 draws the sprite by calling
.draw()
on theActor
object. This draws the sprite image on the screen at the location provided by.pos
.
You’ll use these techniques in a more advanced game next.
Advanced Application
To demonstrate the difference between the game engines, you’ll revisit the same advanced game that you saw in the Pygame section, now written using Pygame Zero. As a reminder, the key details of that game are:
- The player is a single sprite on the screen, controlled by moving the mouse.
- At regular intervals, coins appear on the screen one by one.
- As the player moves over each coin, it disappears and the player is awarded ten points.
- As the game progresses, coins are added more quickly.
- The game ends when there are more than ten coins visible on the screen.
This game should look and behave identically to the Pygame version demonstrated earlier, with only the window title bar betraying the Pygame Zero origin:
You can find the complete code for this sample in the downloaded materials and below:
1"""
2Complete game in Pygame Zero
3
4This game demonstrates some of the more advanced features of
5Pygame Zero, including:
6- Using sprites to render complex graphics
7- Handling user input
8- Sound output
9
10"""
11
12# Import pgzrun allows the program to run in Python IDLE
13import pgzrun
14
15# For type-hinting support
16from typing import Tuple
17
18# To randomize coin placement
19from random import randint
20
21# Set the width and height of your output window, in pixels
22WIDTH = 800
23HEIGHT = 600
24
25# Set up the player
26player = Actor("alien_green_stand")
27player_position = WIDTH // 2, HEIGHT // 2
28player.center = player_position
29
30# Set up the coins to collect
31COIN_COUNT = 10
32coin_list = list()
33
34# Set up a timer to create new coins
35coin_countdown = 2.5
36coin_interval = 0.1
37
38# Score is initially zero
39score = 0
40
41def add_coin():
42 """Adds a new coin to playfield, then
43 schedules the next coin to be added
44 """
45 global coin_countdown
46
47 # Create a new coin Actor at a random location
48 new_coin = Actor(
49 "coin_gold", (randint(10, WIDTH - 10), randint(10, HEIGHT - 10))
50 )
51
52 # Add it to the global coin list
53 coin_list.append(new_coin)
54
55 # Decrease the time between coin appearances if there are
56 # fewer than three coins on the screen.
57 if len(coin_list) < 3:
58 coin_countdown -= coin_interval
59
60 # Make sure you don't go too quickly
61 if coin_countdown < 0.1:
62 coin_countdown = 0.1
63
64 # Schedule the next coin addition
65 clock.schedule(add_coin, coin_countdown)
66
67def on_mouse_move(pos: Tuple):
68 """Called whenever the mouse changes position
69
70 Arguments:
71 pos {Tuple} -- The current position of the mouse
72 """
73 global player_position
74
75 # Set the player to the mouse position
76 player_position = pos
77
78 # Ensure the player doesn't move off the screen
79 if player_position[0] < 0:
80 player_position[0] = 0
81 if player_position[0] > WIDTH:
82 player_position[0] = WIDTH
83
84 if player_position[1] < 0:
85 player_position[1] = 0
86 if player_position[1] > HEIGHT:
87 player_position[1] = HEIGHT
88
89def update(delta_time: float):
90 """Called every frame to update game objects
91
92 Arguments:
93 delta_time {float} -- Time since the last frame
94 """
95 global score
96
97 # Update the player position
98 player.center = player_position
99
100 # Check if the player has collided with a coin
101 # First, set up a list of coins to remove
102 coin_remove_list = []
103
104 # Check each coin in the list for a collision
105 for coin in coin_list:
106 if player.colliderect(coin):
107 sounds.coin_pickup.play()
108 coin_remove_list.append(coin)
109 score += 10
110
111 # Remove any coins with which you collided
112 for coin in coin_remove_list:
113 coin_list.remove(coin)
114
115 # The game is over when there are too many coins on the screen
116 if len(coin_list) >= COIN_COUNT:
117 # Stop making new coins
118 clock.unschedule(add_coin)
119
120 # Print the final score and exit the game
121 print(f"Game over! Final score: {score}")
122 exit()
123
124def draw():
125 """Render everything on the screen once per frame"""
126
127 # Clear the screen first
128 screen.clear()
129
130 # Set the background color to pink
131 screen.fill("pink")
132
133 # Draw the remaining coins
134 for coin in coin_list:
135 coin.draw()
136
137 # Draw the player
138 player.draw()
139
140 # Draw the current score at the bottom
141 screen.draw.text(
142 f"Score: {score}",
143 (50, HEIGHT - 50),
144 fontsize=48,
145 color="black",
146 )
147
148# Schedule the first coin to appear
149clock.schedule(add_coin, coin_countdown)
150
151# Run the program
152pgzrun.go()
Creating the player Actor
is done on lines 26 to 28. The initial position is the center of the screen.
The clock.schedule()
method handles creating coins at regular intervals. This method takes a function to call and the number of seconds to delay before calling that function.
Lines 41 to 65 define the add_coin()
function that will be scheduled. It creates a new coin Actor
at a random location on lines 48 to 50 and adds it to a global list of visible coins.
As the game progresses, coins should appear more and more quickly, but not too quickly. Managing the interval is done on lines 57 to 62. Because clock.schedule()
will only fire a single time, you schedule another call on line 65.
Mouse movement is processed in the on_mouse_move()
event handler on lines 67 to 87. The mouse position is captured and stored in a global variable on line 76. Lines 79 to 87 ensure this position is never off the screen.
The update()
function defined on lines 89 to 122 is called once per frame by Pygame Zero. You use this to move Actor
objects and update the state of all your game objects. The position of the player Actor
is updated to track the mouse on line 98.
Collisions with coins are handled on lines 102 to 113. If the player has collided with a coin, then the coin is added to coin_remove_list
, the score is incremented, and a sound is played. When all the collisions have been processed, you remove the coins which were added to coin_remove_list
on lines 112 to 113.
After coin collisions are handled, you check to see if there are still too many coins on the screen on line 116. If so, the game is over, so you stop creating new coins, print the final score, and end the game on lines 118 to 122.
Of course, all this updating needs to be reflected on the screen. The draw()
function on lines 124 to 146 is called after update()
once per frame. After clearing the screen and filling it with a background color on lines 128 and 131, the player Actor
and all the coins are drawn on lines 134 to 138. The current score is the last thing drawn on lines 141 to 146.
The Pygame Zero implementation used 152 lines of code to deliver the same game as 182 lines of Pygame code. While these line counts are comparable, the Pygame Zero version is arguably cleaner, more modular, and possibly easier to understand and code than the Pygame version.
Of course, there’s always one more way to write a game.
Arcade
Arcade is a modern Python framework for crafting games with compelling graphics and sound. Object-oriented by design, Arcade provides game authors with a modern set of tools for crafting great Python game experiences.
Designed by Professor Paul Craven from Simpson College in Iowa, USA, Arcade is built on top of the pyglet windowing and multimedia library. It provides a set of improvements, modernizations, and enhancements that compare favorably with both Pygame and Pygame Zero:
- Supports modern OpenGL graphics
- Supports Python 3 type hinting
- Has support for frame-based animated sprites
- Incorporates consistent command, function, and parameter names
- Encourages separation of game logic from display code
- Requires less boilerplate code
- Provides well-maintained and up-to-date documentation, including several tutorials and complete Python game examples
- Has built-in physics engines for top-down and platform games
Arcade is under constant development, is well supported in the community, and has an author who’s very responsive to issues, bug reports, and potential fixes.
Arcade Installation
To install Arcade and its dependencies, use the appropriate pip
command:
(venv) $ python -m pip install arcade
Complete installation instructions based on your platform are available for Windows, macOS, and Linux. You can even install arcade
directly from source if you’d prefer.
Basic Concepts
Everything in Arcade occurs in a window that’s created with a user-defined size. The coordinate system assumes that the origin (0, 0)
is located in the lower-left corner of the screen, with the y-coordinates increasing as you move up. This is different from many other game engines, which place (0, 0)
in the upper left and increase the y-coordinates moving down.
At its heart, Arcade is an object-oriented library. While it’s possible to write Arcade applications procedurally, its real power is revealed when you create fully object-oriented code.
Arcade, like Pygame Zero, provides a built-in game loop and a well-defined event model, so you end up with very clean and readable game code. Also like Pygame Zero, Arcade provides a powerful sprite class which aids rendering, positioning, and collision detection. In addition, Arcade sprites can be animated by providing multiple images.
The code for a basic Arcade application listed below is provided in the tutorial’s source code as arcade_basic.py
:
1"""
2Basic "Hello, World!" program in Arcade
3
4This program is designed to demonstrate the basic capabilities
5of Arcade. It will:
6- Create a game window
7- Fill the background with white
8- Draw some basic shapes in different colors
9- Draw some text in a specified size and color
10"""
11
12# Import arcade allows the program to run in Python IDLE
13import arcade
14
15# Set the width and height of your output window, in pixels
16WIDTH = 800
17HEIGHT = 600
18
19# Classes
20class ArcadeBasic(arcade.Window):
21 """Main game window"""
22
23 def __init__(self, width: int, height: int, title: str):
24 """Initialize the window to a specific size
25
26 Arguments:
27 width {int} -- Width of the window
28 height {int} -- Height of the window
29 title {str} -- Title for the window
30 """
31
32 # Call the parent class constructor
33 super().__init__(width, height, title)
34
35 # Set the background window
36 arcade.set_background_color(color=arcade.color.WHITE)
37
38 def on_draw(self):
39 """Called once per frame to render everything on the screen"""
40
41 # Start rendering
42 arcade.start_render()
43
44 # Draw a blue circle with a radius of 50 in the center of the screen
45 arcade.draw_circle_filled(
46 center_x=WIDTH // 2,
47 center_y=HEIGHT // 2,
48 radius=50,
49 color=arcade.color.BLUE,
50 num_segments=50,
51 )
52
53 # Draw a red-outlined square in the top-left corner of the screen
54 arcade.draw_lrtb_rectangle_outline(
55 left=50,
56 top=HEIGHT - 50,
57 bottom=HEIGHT - 100,
58 right=100,
59 color=arcade.color.RED,
60 border_width=3,
61 )
62
63 # Draw an orange caption along the bottom in 60-point font
64 arcade.draw_text(
65 text="Hello, World! From Arcade!",
66 start_x=100,
67 start_y=50,
68 font_size=28,
69 color=arcade.color.ORANGE,
70 )
71
72# Run the program
73if __name__ == "__main__":
74 arcade_game = ArcadeBasic(WIDTH, HEIGHT, "Arcade Basic Game")
75 arcade.run()
To run this code, use the following command:
(venv) $ python arcade/arcade_basic.py
This program draws a few shapes and some text on the screen, as in the basic examples previously shown:
As mentioned above, Arcade programs can be written as fully object-oriented code. The arcade.Window
class is designed to be subclassed by your games, as shown on line 20. Calling super().__init()
on line 33 ensures the game window is set up properly.
Arcade calls the .on_draw()
event handler defined on lines 38 to 70 once per frame to render everything to the screen. This method starts with a call to .start_render()
, which tells Arcade to prepare the window for drawing. This is comparable to the pygame.flip()
call required at the end of the Pygame drawing step.
Each of the basic shape-drawing methods in Arcade starts with draw_*
and requires a single line to complete. Arcade has built-in drawing support for numerous shapes.
Arcade comes loaded with hundreds of named colors in the arcade.color
package, but you’re also free to pick your own colors using RGB or RGBA tuples.
Advanced Application
To show how Arcade is different from other game engines, you’ll see the same game from before, now implemented in Arcade. As a reminder, here are the key details of the game:
- The player is a single sprite on the screen, controlled by moving the mouse.
- At regular intervals, coins appear on the screen one by one.
- As the player moves over each coin, it disappears and the player is awarded ten points.
- As the game progresses, coins are added more quickly.
- The game ends when there are more than ten coins visible on the screen.
Again, the game should act the same as the previous examples:
The code for the full Arcade game listed below is provided in the downloadable materials as arcade_game.py
:
1"""
2Complete game in Arcade
3
4This game demonstrates some of the more advanced features of
5Arcade, including:
6- Using sprites to render complex graphics
7- Handling user input
8- Sound output
9"""
10
11# Import arcade allows the program to run in Python IDLE
12import arcade
13
14# To randomize coin placement
15from random import randint
16
17# To locate your assets
18from pathlib import Path
19
20# Set the width and height of your game window, in pixels
21WIDTH = 800
22HEIGHT = 600
23
24# Set the game window title
25TITLE = "Arcade Sample Game"
26
27# Location of your assets
28ASSETS_PATH = Path.cwd() / "assets"
29
30# How many coins must be on the screen before the game is over?
31COIN_COUNT = 10
32
33# How much is each coin worth?
34COIN_VALUE = 10
35
36# Classes
37class ArcadeGame(arcade.Window):
38 """The Arcade Game class"""
39
40 def __init__(self, width: float, height: float, title: str):
41 """Create the main game window
42
43 Arguments:
44 width {float} -- Width of the game window
45 height {float} -- Height of the game window
46 title {str} -- Title for the game window
47 """
48
49 # Call the super class init method
50 super().__init__(width, height, title)
51
52 # Set up a timer to create new coins
53 self.coin_countdown = 2.5
54 self.coin_interval = 0.1
55
56 # Score is initially zero
57 self.score = 0
58
59 # Set up empty sprite lists
60 self.coins = arcade.SpriteList()
61
62 # Don't show the mouse cursor
63 self.set_mouse_visible(False)
64
65 def setup(self):
66 """Get the game ready to play"""
67
68 # Set the background color
69 arcade.set_background_color(color=arcade.color.PINK)
70
71 # Set up the player
72 sprite_image = ASSETS_PATH / "images" / "alien_green_stand.png"
73 self.player = arcade.Sprite(
74 filename=sprite_image, center_x=WIDTH // 2, center_y=HEIGHT // 2
75 )
76
77 # Spawn a new coin
78 arcade.schedule(
79 function_pointer=self.add_coin, interval=self.coin_countdown
80 )
81
82 # Load your coin collision sound
83 self.coin_pickup_sound = arcade.load_sound(
84 ASSETS_PATH / "sounds" / "coin_pickup.wav"
85 )
86
87 def add_coin(self, dt: float):
88 """Add a new coin to the screen, reschedule the timer if necessary
89
90 Arguments:
91 dt {float} -- Time since last call (unused)
92 """
93
94 # Create a new coin
95 coin_image = ASSETS_PATH / "images" / "coin_gold.png"
96 new_coin = arcade.Sprite(
97 filename=coin_image,
98 center_x=randint(20, WIDTH - 20),
99 center_y=randint(20, HEIGHT - 20),
100 )
101
102 # Add the coin to the current list of coins
103 self.coins.append(new_coin)
104
105 # Decrease the time between coin appearances, but only if there are
106 # fewer than three coins on the screen.
107 if len(self.coins) < 3:
108 self.coin_countdown -= self.coin_interval
109
110 # Make sure you don't go too quickly
111 if self.coin_countdown < 0.1:
112 self.coin_countdown = 0.1
113
114 # Stop the previously scheduled call
115 arcade.unschedule(function_pointer=self.add_coin)
116
117 # Schedule the next coin addition
118 arcade.schedule(
119 function_pointer=self.add_coin, interval=self.coin_countdown
120 )
121
122 def on_mouse_motion(self, x: float, y: float, dx: float, dy: float):
123 """Processed when the mouse moves
124
125 Arguments:
126 x {float} -- X Position of the mouse
127 y {float} -- Y Position of the mouse
128 dx {float} -- Change in x position since last move
129 dy {float} -- Change in y position since last move
130 """
131
132 # Ensure the player doesn't move off-screen
133 self.player.center_x = arcade.clamp(x, 0, WIDTH)
134 self.player.center_y = arcade.clamp(y, 0, HEIGHT)
135
136 def on_update(self, delta_time: float):
137 """Update all the game objects
138
139 Arguments:
140 delta_time {float} -- How many seconds since the last frame?
141 """
142
143 # Check if you've picked up a coin
144 coins_hit = arcade.check_for_collision_with_list(
145 sprite=self.player, sprite_list=self.coins
146 )
147
148 for coin in coins_hit:
149 # Add the coin score to your score
150 self.score += COIN_VALUE
151
152 # Play the coin sound
153 arcade.play_sound(self.coin_pickup_sound)
154
155 # Remove the coin
156 coin.remove_from_sprite_lists()
157
158 # Are there more coins than allowed on the screen?
159 if len(self.coins) > COIN_COUNT:
160 # Stop adding coins
161 arcade.unschedule(function_pointer=self.add_coin)
162
163 # Show the mouse cursor
164 self.set_mouse_visible(True)
165
166 # Print the final score and exit the game
167 print(f"Game over! Final score: {self.score}")
168 exit()
169
170 def on_draw(self):
171 """Draw everything"""
172
173 # Start the rendering pass
174 arcade.start_render()
175
176 # Draw the coins
177 self.coins.draw()
178
179 # Draw the player
180 self.player.draw()
181
182 # Draw the score in the lower-left corner
183 arcade.draw_text(
184 text=f"Score: {self.score}",
185 start_x=50,
186 start_y=50,
187 font_size=32,
188 color=arcade.color.BLACK,
189 )
190
191if __name__ == "__main__":
192 arcade_game = ArcadeGame(WIDTH, HEIGHT, TITLE)
193 arcade_game.setup()
194 arcade.run()
The object-oriented nature of Arcade allows you to quickly implement different levels by separating the initialization of the game from the initialization of each different level. The game is initialized in the .__init__()
method on lines 40 to 63, while levels are set up and can be restarted using the separate .setup()
method on lines 65 to 85. This is a great pattern to use even for games that have a single level, like this one.
Sprites are defined by creating an object of the class arcade.Sprite
, and providing the path to an image. Arcade supports pathlib paths, which eases the creation of the player sprite on lines 72 to 75.
Creating new coins is handled on lines 78 to 80, which call arcade.schedule()
to call the self.add_coin()
method at regular intervals.
The .add_coin()
method defined on lines 87 to 120 creates a new coin sprite at a random location and adds it to a list to simplify drawing as well as collision handling later.
To move the player using the mouse, you implement the .on_mouse_motion()
method on lines 122 to 134. The arcade.clamp()
method ensures the player’s center coordinates are never off the screen.
Checking for collisions between the player and the coin is handled in the .on_update()
method on lines 144 to 156. The arcade.check_for_collision_with_list()
method returns a list of all the sprites in the list that have collided with the specified sprite. The code walks through that list, incrementing the score and playing a sound effect before removing each coin from play.
The .on_update()
method also checks if there are too many coins on the screen on lines 159 to 168. If so, it ends the game.
This Arcade implementation is just as readable and well structured as the Pygame Zero code, although it took over 27% more code, with 194 lines written. The longer code may be worth it, as Arcade offers many more features not demonstrated here, such as:
- Animated sprites
- Several built-in physics engines
- Support for third-party game maps
- Updated particle and shader systems
New game authors coming from Python Zero will find Arcade similar in structure while offering more powerful and extensive features.
adventurelib
Of course, not every game requires a colorful player moving on the screen, avoiding obstacles, and killing bad guys. Classic computer games like Zork showed off the power of good storytelling while still providing a great gaming experience. Crafting these text-based games, also called interactive fiction, can be difficult in any language. Luckily for the Python programmer, there’s adventurelib:
adventurelib provides basic functionality for writing text-based adventure games, with the aim of making it easy enough for young teenagers to do. (Source)
It’s not just for teenagers, though! adventurelib is great for anyone who wants to write a text-based game without having to also write a natural language parser to do so.
adventurelib was created by the folks behind Pygame Zero, and it tackles more advanced computer science topics such as state management, business logic, naming and references, and set manipulation, to name a few. This makes it a great next step for educators, parents, and mentors helping young people learn computer science through the creation of games. It’s also great for broadening your own game-coding skills.
adventurelib Installation
adventurelib is available on PyPI and can be installed using the appropriate pip
command:
(venv) $ python -m pip install adventurelib
adventurelib is a single file, so it can also be downloaded from the GitHub repo, saved in the same folder as your game, and used directly.
Basic Concepts
To learn the basics of adventurelib, you’ll see a small game with three rooms and a key to unlock a door to the final room below. The code for this sample game is provided in the downloadable materials in adventurelib_basic.py
:
1"""
2Basic "Hello, World!" program in adventurelib
3
4This program is designed to demonstrate the basic capabilities
5of adventurelib. It will:
6- Create a basic three-room world
7- Add a single inventory item
8- Require that inventory item to move to the final room
9"""
10
11# Import the library contents
12import adventurelib as adv
13
14# Define your rooms
15bedroom = adv.Room(
16 """
17You are in your bedroom. The bed is unmade, but otherwise
18it's clean. Your dresser is in the corner, and a desk is
19under the window.
20"""
21)
22
23living_room = adv.Room(
24 """
25The living room stands bright and empty. The TV is off,
26and the sun shines brightly through the curtains.
27"""
28)
29
30front_porch = adv.Room(
31 """
32The creaky boards of your front porch welcome you as an
33old friend. Your front door mat reads 'Welcome'.
34"""
35)
36
37# Define the connections between the rooms
38bedroom.south = living_room
39living_room.east = front_porch
40
41# Define a constraint to move from the bedroom to the living room
42# If the door between the living room and front porch door is locked,
43# you can't exit
44living_room.locked = {"east": True}
45
46# None of the other rooms have any locked doors
47bedroom.locked = dict()
48front_porch.locked = dict()
49
50# Set the starting room as the current room
51current_room = bedroom
52
53# Define functions to use items
54def unlock_living_room(current_room):
55
56 if current_room == living_room:
57 print("You unlock the door.")
58 current_room.locked["east"] = False
59 else:
60 print("There is nothing to unlock here.")
61
62# Create your items
63key = adv.Item("a front door key", "key")
64key.use_item = unlock_living_room
65
66# Create empty Bags for room contents
67bedroom.contents = adv.Bag()
68living_room.contents = adv.Bag()
69front_porch.contents = adv.Bag()
70
71# Put the key in the bedroom
72bedroom.contents.add(key)
73
74# Set up your current empty inventory
75inventory = adv.Bag()
76
77# Define your movement commands
78@adv.when("go DIRECTION")
79@adv.when("north", direction="north")
80@adv.when("south", direction="south")
81@adv.when("east", direction="east")
82@adv.when("west", direction="west")
83@adv.when("n", direction="north")
84@adv.when("s", direction="south")
85@adv.when("e", direction="east")
86@adv.when("w", direction="west")
87def go(direction: str):
88 """Processes your moving direction
89
90 Arguments:
91 direction {str} -- which direction does the player want to move
92 """
93
94 # What is your current room?
95 global current_room
96
97 # Is there an exit in that direction?
98 next_room = current_room.exit(direction)
99 if next_room:
100 # Is the door locked?
101 if direction in current_room.locked and current_room.locked[direction]:
102 print(f"You can't go {direction} --- the door is locked.")
103 else:
104 current_room = next_room
105 print(f"You go {direction}.")
106 look()
107
108 # No exit that way
109 else:
110 print(f"You can't go {direction}.")
111
112# How do you look at the room?
113@adv.when("look")
114def look():
115 """Looks at the current room"""
116
117 # Describe the room
118 adv.say(current_room)
119
120 # List the contents
121 for item in current_room.contents:
122 print(f"There is {item} here.")
123
124 # List the exits
125 print(f"The following exits are present: {current_room.exits()}")
126
127# How do you look at items?
128@adv.when("look at ITEM")
129@adv.when("inspect ITEM")
130def look_at(item: str):
131
132 # Check if the item is in your inventory or not
133 obj = inventory.find(item)
134 if not obj:
135 print(f"You don't have {item}.")
136 else:
137 print(f"It's an {obj}.")
138
139# How do you pick up items?
140@adv.when("take ITEM")
141@adv.when("get ITEM")
142@adv.when("pickup ITEM")
143def get(item: str):
144 """Get the item if it exists
145
146 Arguments:
147 item {str} -- The name of the item to get
148 """
149 global current_room
150
151 obj = current_room.contents.take(item)
152 if not obj:
153 print(f"There is no {item} here.")
154 else:
155 print(f"You now have {item}.")
156 inventory.add(obj)
157
158# How do you use an item?
159@adv.when("unlock door", item="key")
160@adv.when("use ITEM")
161def use(item: str):
162 """Use an item, consumes it if used
163
164 Arguments:
165 item {str} -- Which item to use
166 """
167
168 # First, do you have the item?
169 obj = inventory.take(item)
170 if not obj:
171 print(f"You don't have {item}")
172
173 # Try to use the item
174 else:
175 obj.use_item(current_room)
176
177if __name__ == "__main__":
178 # Look at the starting room
179 look()
180
181 adv.start()
To run this code, use the following command:
(venv) $ python adventurelib/adventurelib_basic.py
Text-based games rely heavily on parsing user input to drive the game forward. adventurelib defines the text that a player types as a command and provides the @when()
decorator to define commands.
A good example of a command is the look
command defined on lines 113 to 125. The @when("look")
decorator adds the text look
to a list of valid commands and connects it to the look()
function. Whenever the player types look
, adventurelib will call the look()
function.
Commands are case-insensitive when typed by the player. The player can type look
, LOOK
, Look
, or even lOOk
, and adventurelib will find the correct command.
Multiple commands can all use the same function, as seen with the go()
function on lines 78 to 110. This function is decorated with nine separate commands, allowing the player several different ways to move around the game world. In the game play example below, the commands south
, east
, and north
are all used, but each results in the same function being called:
Sometimes the commands that a player types are directed at a specific item. For example, the player may want to look at a particular thing or go in a specific direction. The game designer can capture additional command context by specifying capitalized words in the @when()
decorator. These are treated as variable names, and the text that the player types in their place are the values.
This can be seen in the look_at()
function on lines 128 to 137. This function defines a single string parameter called item
. In the @when()
decorators defining the look at
and inspect
commands, the word ITEM
acts as a placeholder for any text following the command. This text is then passed to the look_at()
function as the item
parameter. For example, if the player types look at book
, then the parameter item
will get the value "book"
.
The strength of a text-based game relies on the descriptiveness of its text. While you can and should certainly use print()
functions, printing numerous lines of text in response to user commands can introduce difficulties spanning text over multiple lines and determining line breaks. adventurelib eases this burden with the say()
function, which works well with triple-quoted multiline strings.
You can see the say()
function in action on line 118 in the look()
function. Whenever the player types look
, the say()
function outputs the description of the current room to the console.
Of course, your commands need places to occur. adventurelib provides the Room
class to define different areas of your game world. Rooms are created by providing a description of the room, and they can be connected to other rooms by using the .north
, .south
, .east
, and .west
properties. You can also define custom properties that apply to either the entire Room
class or individual objects.
The three rooms in this game are created on lines 15 to 35. The Room()
constructor accepts a description as a string, or in this case, as a multiline string. Once you’ve created the rooms, then you connect them on lines 38 to 39. Setting bedroom.south
to living_room
implies that living_room.north
will be bedroom
. adventurelib is smart enough to make this connection automatically.
You also create a constraint on line 44 to indicate a locked door between the living room and the front porch. Unlocking this door will require the player to locate an item.
Text-based games often feature items which must be collected to open new areas of the game or to solve certain puzzles. Items can also represent non-player characters with whom the player can interact. adventurelib provides the Item
class to define both collectable items and non-player characters by their names and aliases. For example, the alias key
refers to the front door key:
On line 63, you define the key
used to unlock the door between the living room and the front porch. The Item()
constructor takes one or more strings. The first is the default or full name of the item, and it’s used when printing the name of the item. All other names are used as aliases so the player doesn’t have to type the full name of the item.
The key
doesn’t just have a name and aliases. It also has an intended use, which is defined on line 64. key.use_item
refers to a function that will be called when a player tries to use the item by typing "use key"
. This function is called in the use()
command handler defined on lines 159 to 175.
Collections of items, such as the player’s inventory or items on the ground in a room, can be stored in a Bag
object. You can add items to the bag, remove items from the bag, and inspect the bag’s contents. Bag
objects are iterable in Python, so you can also use in
to test if something is in the bag and loop over the bag’s contents in a for
loop.
Four different Bag
objects are defined on lines 67 to 75. Each of the three rooms has a Bag
to hold items in the room, and the player also has a Bag
to hold their inventory
of items they pick up. The key
item is placed in its starting location in the bedroom
.
Items are added to the player’s inventory by the get()
function defined on lines 140 to 156. When the player types get key
, you attempt to take()
the item from the room’s contents
bag on line 151. If the key
is returned, it’s also removed from the room’s contents
. You then add the key
to the player’s inventory
on line 156.
Advanced Application
Of course, there’s much more to adventurelib. To show off its other capabilities, you’ll craft a more involved text adventure with the following backstory:
- You live in a small, quiet hamlet.
- Recently, your neighbors have begun complaining of missing livestock.
- As a member of a night patrol, you notice a broken fence and a trail leading away from it.
- You decide to investigate, armed only with a wooden practice sword.
The game has several areas to describe and define:
- Your small, quiet hamlet
- The trail leading away from the field
- A nearby village where you can buy a better weapon
- A side path leading to a wizard who can enchant your weapon
- A cave containing the giant who has been taking your livestock
There are several items to collect, such as weapons and food, and characters with which to interact. You also need a basic battle system to allow you to fight the giant and win the game.
All of the code for this game is listed below, and can be found in the downloaded materials:
To keep things organized, you break your game into different files:
adventurelib_game_rooms.py
defines the rooms and areas.adventurelib_game_items.py
defines the items and their attributes.adventurelib_game_characters.py
defines the characters with which you can interact.adventurelib_game.py
pulls everything together, adds commands, and starts the game.
1"""
2Complete game written in adventurelib
3
4This program is designed to demonstrate the capabilities
5of adventurelib. It will:
6- Create a large world in which to wander
7- Contain several inventory items
8- Set contexts for moving from one area to another
9- Require some puzzle-solving skills
10"""
11
12# Import the library contents
13# from adventurelib import *
14import adventurelib as adv
15
16# Import your rooms, which imports your items and characters
17import adventurelib_game_rooms
18
19import adventurelib_game_items
20
21# For your battle sequence
22from random import randint
23
24# To allow you to exit the game
25import sys
26
27# Set the first room
28current_room = adventurelib_game_rooms.home
29current_room.visited = False
30
31# How many HP do you have?
32hit_points = 20
33
34# How many HP does the giant have?
35giant_hit_points = 50
36
37# Your current inventory
38inventory = adv.Bag()
39
40# Some basic item commands
41@adv.when("inventory")
42@adv.when("inv")
43@adv.when("i")
44def list_inventory():
45 if inventory:
46 print("You have the following items:")
47 for item in inventory:
48 print(f" - {item.description}")
49 else:
50 print("You have nothing in your inventory.")
51
52@adv.when("look at ITEM")
53def look_at(item: str):
54 """Prints a short description of an item if it is either:
55 1. in the current room, or
56 2. in our inventory
57
58 Arguments:
59 item {str} -- the item to look at
60 """
61
62 global inventory, current_room
63
64 # Check if the item is in the room
65 obj = current_room.items.find(item)
66 if not obj:
67 # Check if the item is in your inventory
68 obj = inventory.find(item)
69 if not obj:
70 print(f"I can't find {item} anywhere.")
71 else:
72 print(f"You have {item}.")
73 else:
74 print(f"You see {item}.")
75
76@adv.when("describe ITEM")
77def describe(item: str):
78 """Prints a description of an item if it is either:
79 1. in the current room, or
80 2. in your inventory
81
82 Arguments:
83 item {str} -- the item to look at
84 """
85
86 global inventory, current_room
87
88 # Check if the item is in the room
89 obj = current_room.items.find(item)
90 if not obj:
91 # Check if the item is in your inventory
92 obj = inventory.find(item)
93 if not obj:
94 print(f"I can't find {item} anywhere.")
95 else:
96 print(f"You have {obj.description}.")
97 else:
98 print(f"You see {obj.description}.")
99
100@adv.when("take ITEM")
101@adv.when("get ITEM")
102@adv.when("pickup ITEM")
103@adv.when("pick up ITEM")
104@adv.when("grab ITEM")
105def take_item(item: str):
106 global current_room
107
108 obj = current_room.items.take(item)
109 if not obj:
110 print(f"I don't see {item} here.")
111 else:
112 print(f"You now have {obj.description}.")
113 inventory.add(obj)
114
115@adv.when("eat ITEM")
116def eat(item: str):
117 global inventory
118
119 # Make sure you have the thing first
120 obj = inventory.find(item)
121
122 # Do you have this thing?
123 if not obj:
124 print(f"You don't have {item}.")
125
126 # Is it edible?
127 elif obj.edible:
128 print(f"You savor every bite of {obj.description}.")
129 inventory.take(item)
130
131 else:
132 print(f"How do you propose we eat {obj.description}?")
133
134@adv.when("wear ITEM")
135@adv.when("put on ITEM")
136def wear(item: str):
137 global inventory
138
139 # Make sure you have the thing first
140 obj = inventory.find(item)
141
142 # Do you have this thing?
143 if not obj:
144 print(f"You don't have {item}.")
145
146 # Is it wearable?
147 elif obj.wearable:
148 print(f"The {obj.description} makes a wonderful fashion statement!")
149
150 else:
151 print(
152 f"""This is no time for avant garde fashion choices!
153 Wear a {obj.description}? Really?"""
154 )
155
156# Some character-specific commands
157@adv.when("talk to CHARACTER")
158def talk_to(character: str):
159 global current_room
160
161 char = current_room.characters.find(character)
162
163 # Is the character there?
164 if not char:
165 print(f"Sorry, I can't find {character}.")
166
167 # It's a character who is there
168 else:
169 # Set the context, and start the encounter
170 adv.set_context(char.context)
171 adv.say(char.greeting)
172
173@adv.when("yes", context="elder")
174def yes_elder():
175 global current_room
176
177 adv.say(
178 """
179 It is not often one of our number leaves, and rarer still if they leave
180 to defend our Home. Go with our blessing, and our hope for a successful
181 journey and speedy return. To help, we bestow three gifts.
182
183 The first is one of knowledge. There is a blacksmith in one of the
184 neighboring villages. You may find help there.
185
186 Second, seek a wizard who lives as a hermit, who may be persuaded to
187 give aid. Be wary, though! The wizard does not give away his aid for
188 free. As he tests you, remember always where you started your journey.
189
190 Lastly, we don't know what dangers you may face. We are peaceful people,
191 but do not wish you to go into the world undefended. Take this meager
192 offering, and use it well!
193 """
194 )
195 inventory.add(adventurelib_game_items.wooden_sword)
196 current_room.locked_exits["south"] = False
197
198@adv.when("thank you", context="elder")
199@adv.when("thanks", context="elder")
200def thank_elder():
201 adv.say("It is we who should thank you. Go with our love and hopes!")
202
203@adv.when("yes", context="blacksmith")
204def yes_blacksmith():
205 global current_room
206
207 adv.say(
208 """
209 I can see you've not a lot of money. Usually, everything here
210 if pretty expensive, but I just might have something...
211
212 There's this steel sword here, if you want it. Don't worry --- it
213 doesn't cost anything! It was dropped off for repair a few weeks
214 ago, but the person never came back for it. It's clean, sharp,
215 well-oiled, and will do a lot more damage than that
216 fancy sword-shaped club you've got. I need it gone to clear some room.
217
218 If you want, we could trade even up --- the wooden sword for the
219 steel one. I can use yours for fire-starter. Deal?
220 """
221 )
222 adv.set_context("blacksmith.trade")
223
224@adv.when("yes", context="blacksmith.trade")
225def trade_swords_yes():
226 print("Great!")
227 inventory.take("wooden sword")
228 inventory.add(adventurelib_game_items.steel_sword)
229
230@adv.when("no", context="blacksmith.trade")
231def trade_swords_no():
232 print("Well, that's all I have within your budget. Good luck!")
233 adv.set_context(None)
234
235@adv.when("yes", context="wizard")
236def yes_wizard():
237 global current_room
238
239 adv.say(
240 """
241 I can make your weapon more powerful than it is, but only if
242 you can answer my riddle:
243
244 What has one head...
245 One foot...
246 But four legs?
247 """
248 )
249
250 adv.set_context("wizard.riddle")
251
252@adv.when("bed", context="wizard.riddle")
253@adv.when("a bed", context="wizard.riddle")
254def answer_riddle():
255 adv.say("You are smarter than you believe yourself to be! Behold!")
256
257 obj = inventory.find("sword")
258 obj.bonus = 2
259 obj.description += ", which glows with eldritch light"
260
261 adv.set_context(None)
262 current_room.locked_exits["west"] = False
263
264@adv.when("fight CHARACTER", context="giant")
265def fight_giant(character: str):
266
267 global giant_hit_points, hit_points
268
269 sword = inventory.find("sword")
270
271 # The player gets a swing
272 player_attack = randint(1, sword.damage + 1) + sword.bonus
273 print(f"You swing your {sword}, doing {player_attack} damage!")
274 giant_hit_points -= player_attack
275
276 # Is the giant dead?
277 if giant_hit_points <= 0:
278 end_game(victory=True)
279
280 print_giant_condition()
281 print()
282
283 # Then the giant tries
284 giant_attack = randint(0, 5)
285 if giant_attack == 0:
286 print("The giant's arm whistles harmlessly over your head!")
287 else:
288 print(
289 f"""
290 The giant swings his mighty fist,
291 and does {giant_attack} damage!
292 """
293 )
294 hit_points -= giant_attack
295
296 # Is the player dead?
297 if hit_points <= 0:
298 end_game(victory=False)
299
300 print_player_condition()
301 print()
302
303def print_giant_condition():
304
305 if giant_hit_points < 10:
306 print("The giant staggers, his eyes unfocused.")
307 elif giant_hit_points < 20:
308 print("The giant's steps become more unsteady.")
309 elif giant_hit_points < 30:
310 print("The giant sweats and wipes the blood from his brow.")
311 elif giant_hit_points < 40:
312 print("The giant snorts and grits his teeth against the pain.")
313 else:
314 print("The giant smiles and readies himself for the attack.")
315
316def print_player_condition():
317
318 if hit_points < 4:
319 print("Your eyes lose focus on the giant as you sway unsteadily.")
320 elif hit_points < 8:
321 print(
322 """
323 Your footing becomes less steady
324 as you swing your sword sloppily.
325 """
326 )
327 elif hit_points < 12:
328 print(
329 """
330 Blood mixes with sweat on your face
331 as you wipe it from your eyes.
332 """
333 )
334 elif hit_points < 16:
335 print("You bite down as the pain begins to make itself felt.")
336 else:
337 print("You charge into the fray valiantly!")
338
339def end_game(victory: bool):
340 if victory:
341 adv.say(
342 """
343 The giant falls to his knees as the last of his strength flees
344 his body. He takes one final swing at you, which you dodge easily.
345 His momentum carries him forward, and he lands face down in the dirt.
346 His final breath escapes his lips as he succumbs to your attack.
347
348 You are victorious! Your name will be sung for generations!
349 """
350 )
351
352 else:
353 adv.say(
354 """
355 The giant's mighty fist connects with your head, and the last
356 sound you hear are the bones in your neck crunching. You spin
357 and tumble down, your sword clattering to the floor
358 as the giant laughs.
359 Your eyes see the giant step towards you, his mighty foot
360 raised to crash down on you.
361 Oblivion takes over before you experience anything else...
362
363 You have been defeated! The giant is free to ravage your town!
364 """
365 )
366
367 sys.exit()
368
369@adv.when("flee", context="giant")
370def flee():
371 adv.say(
372 """
373 As you turn to run, the giant reaches out and catches your tunic.
374 He lifts you off the ground, grabbing your dangling sword-arm
375 as he does so. A quick twist, and your sword tumbles to the ground.
376 Still holding you, he reaches his hand to your throat and squeezes,
377 cutting off your air supply.
378
379 The last sight you see before blackness takes you are
380 the rotten teeth of the evil grin as the giant laughs
381 at your puny attempt to stop him...
382
383 You have been defeated! The giant is free to ravage your town!
384 """
385 )
386
387 sys.exit()
388
389@adv.when("goodbye")
390@adv.when("bye")
391@adv.when("adios")
392@adv.when("later")
393def goodbye():
394
395 # Are you fighting the giant?
396 if adv.get_context() == "giant":
397 # Not so fast!
398 print("The giant steps in front of you, blocking your exit!")
399
400 else:
401 # Close the current context
402 adv.set_context(None)
403 print("Fare thee well, traveler!")
404
405# Define some basic commands
406@adv.when("look")
407def look():
408 """Print the description of the current room.
409 If you've already visited it, print a short description.
410 """
411 global current_room
412
413 if not current_room.visited:
414 adv.say(current_room)
415 current_room.visited = True
416 else:
417 print(current_room.short_desc)
418
419 # Are there any items here?
420 for item in current_room.items:
421 print(f"There is {item.description} here.")
422
423@adv.when("describe")
424def describe_room():
425 """Print the full description of the room."""
426 adv.say(current_room)
427
428 # Are there any items here?
429 for item in current_room.items:
430 print(f"There is {item.description} here.")
431
432# Define your movement commands
433@adv.when("go DIRECTION")
434@adv.when("north", direction="north")
435@adv.when("south", direction="south")
436@adv.when("east", direction="east")
437@adv.when("west", direction="west")
438@adv.when("n", direction="north")
439@adv.when("s", direction="south")
440@adv.when("e", direction="east")
441@adv.when("w", direction="west")
442def go(direction: str):
443 """Processes your moving direction
444
445 Arguments:
446 direction {str} -- which direction does the player want to move
447 """
448
449 # What is your current room?
450 global current_room
451
452 # Is there an exit in that direction?
453 next_room = current_room.exit(direction)
454 if next_room:
455 # Is the door locked?
456 if (
457 direction in current_room.locked_exits
458 and current_room.locked_exits[direction]
459 ):
460 print(f"You can't go {direction} --- the door is locked.")
461 else:
462 # Clear the context if necessary
463 current_context = adv.get_context()
464 if current_context == "giant":
465 adv.say(
466 """Your way is currently blocked.
467 Or have you forgotten the giant you are fighting?"""
468 )
469 else:
470 if current_context:
471 print("Fare thee well, traveler!")
472 adv.set_context(None)
473
474 current_room = next_room
475 print(f"You go {direction}.")
476 look()
477
478 # No exit that way
479 else:
480 print(f"You can't go {direction}.")
481
482# Define a prompt
483def prompt():
484 global current_room
485
486 # Get possible exits
487 exits_string = get_exits(current_room)
488
489 # Are you in battle?
490 if adv.get_context() == "giant":
491 prompt_string = f"HP: {hit_points} > "
492 else:
493 prompt_string = f"({current_room.title}) > "
494
495 return f"""({exits_string}) {prompt_string}"""
496
497def no_command_matches(command: str):
498 if adv.get_context() == "wizard.riddle":
499 adv.say("That is not the correct answer. Begone!")
500 adv.set_context(None)
501 current_room.locked_exits["west"] = False
502 else:
503 print(f"What do you mean by '{command}'?")
504
505def get_exits(room):
506 exits = room.exits()
507
508 exits_string = ""
509 for exit in exits:
510 exits_string += f"{exit[0].upper()}|"
511
512 return exits_string[:-1]
513
514# Start the game
515if __name__ == "__main__":
516 # No context is normal
517 adv.set_context(None)
518
519 # Set the prompt
520 adv.prompt = prompt
521
522 # What happens with unknown commands
523 adv.no_command_matches = no_command_matches
524
525 # Look at your starting room
526 look()
527
528 # Start the game
529 adv.start()
1"""
2Rooms for the adventurelib game
3"""
4
5# Import the library contents
6import adventurelib as adv
7
8# Import your items as well
9import adventurelib_game_items
10
11# And your characters
12import adventurelib_game_characters
13
14# Create a subclass of Rooms to track some custom properties
15class GameArea(adv.Room):
16 def __init__(self, description: str):
17
18 super().__init__(description)
19
20 # All areas can have locked exits
21 self.locked_exits = {
22 "north": False,
23 "south": False,
24 "east": False,
25 "west": False,
26 }
27 # All areas can have items in them
28 self.items = adv.Bag()
29
30 # All areas can have characters in them
31 self.characters = adv.Bag()
32
33 # All areas may have been visited already
34 # If so, you can print a shorter description
35 self.visited = False
36
37 # Which means each area needs a shorter description
38 self.short_desc = ""
39
40 # Each area also has a very short title for the prompt
41 self.title = ""
42
43# Your home
44home = GameArea(
45 """
46You wake as the sun streams in through the single
47window into your small room. You lie on your feather bed which
48hugs the north wall, while the remains of last night's
49fire smolders in the center of the room.
50
51Remembering last night's discussion with the council, you
52throw back your blanket and rise from your comfortable
53bed. Cold water awaits you as you splash away the night's
54sleep, grab an apple to eat, and prepare for the day.
55"""
56)
57home.title = "Home"
58home.short_desc = "This is your home."
59
60# Hamlet
61hamlet = GameArea(
62 """
63From the center of your small hamlet, you can see every other
64home. It doesn't really even have an official name --- folks
65around here just call it Home.
66
67The council awaits you as you approach. Elder Barron beckons you
68as you exit your home.
69"""
70)
71hamlet.title = "Hamlet"
72hamlet.short_desc = "You are in the hamlet."
73
74# Fork in road
75fork = GameArea(
76 """
77As you leave your hamlet, you think about how unprepared you
78really are. Your lack of experience and pitiful equipment
79are certainly no match for whatever has been stealing
80the villages livestock.
81
82As you travel, you come across a fork in the path. The path of
83the livestock thief continues east. However, you know
84the village of Dunhaven lies to the west, where you may
85get some additional help.
86"""
87)
88fork.title = "Fork in road"
89fork.short_desc = "You are at a fork in the road."
90
91# Village of Dunhaven
92village = GameArea(
93 """
94A short trek up the well-worn path brings you the village
95of Dunhaven. Larger than your humble Home, Dunhaven sits at
96the end of a supply route from the capitol. As such, it has
97amenities and capabilities not found in the smaller farming
98communities.
99
100As you approach, you hear the clang-clang of hammer on anvil,
101and inhale the unmistakable smell of the coal-fed fire of a
102blacksmith shop to your south.
103"""
104)
105village.title = "Village of Dunhaven"
106village.short_desc = "You are in the village of Dunhaven."
107
108# Blacksmith shop
109blacksmith_shop = GameArea(
110 """
111As you approach the blacksmith, the sounds of the hammer become
112clearer and clearer. Passing the front door, you head towards
113the sound of the blacksmith, and find her busy at the furnace.
114"""
115)
116blacksmith_shop.title = "Blacksmith Shop"
117blacksmith_shop.short_desc = "You are in the blacksmith shop."
118
119# Side path away from fork
120side_path = GameArea(
121 """
122The path leads away from the fork to Dunhaven. Fresh tracks of
123something big, dragging something behind it, lead away to the south.
124"""
125)
126side_path.title = "Side path"
127side_path.short_desc = "You are standing on a side path."
128
129# Wizard's Hut
130wizard_hut = GameArea(
131 """
132The path opens into a shaded glen. A small stream wanders down the
133hills to the east and past an unassuming hut. In front of the hut,
134the local wizard Trent sits smoking a long clay pipe.
135"""
136)
137wizard_hut.title = "Wizard's Hut"
138wizard_hut.short_desc = "You are at the wizard's hut."
139
140# Cave mouth
141cave_mouth = GameArea(
142 """
143The path from Trent's hut follows the stream for a while before
144turning south away from the water. The trees begin closing overhead,
145blocking the sun and lending a chill to the air as you continue.
146
147The path finally terminates at the opening of a large cave. The
148tracks you have been following mix and mingle with others, both
149coming and going, but all the same. Whatever has been stealing
150your neighbor's livestock lives here, and comes and goes frequently.
151"""
152)
153cave_mouth.title = "Cave Mouth"
154cave_mouth.short_desc = "You are at the mouth of large cave."
155
156# Cave of the Giant
157giant_cave = GameArea(
158 """
159You take a few tentative steps into the cave. It feels much warmer
160and more humid than the cold sunless forest air outside. A steady
161drip of water from the rocks is the only sound for a while.
162
163You begin to make out a faint light ahead. You hug the wall and
164press on, as the light becomes brighter. You finally enter a
165chamber at least 20 meters across, with a fire blazing in the center.
166Cages line one wall, some empty, but others containing cows and
167sheep stolen from you neighbors. Opposite them are piles of the bones
168of the creatures unlucky enough to have already been devoured.
169
170As you look around, you become aware of another presence in the room.
171"""
172)
173giant_cave.title = "Cave of the Giant"
174giant_cave.short_desc = "You are in the giant's cave."
175
176# Set up the paths between areas
177home.south = hamlet
178hamlet.south = fork
179fork.west = village
180fork.east = side_path
181village.south = blacksmith_shop
182side_path.south = wizard_hut
183wizard_hut.west = cave_mouth
184cave_mouth.south = giant_cave
185
186# Lock some exits, since you can't leave until something else happens
187hamlet.locked_exits["south"] = True
188wizard_hut.locked_exits["west"] = True
189
190# Place items in different areas
191# These are just for flavor
192home.items.add(adventurelib_game_items.apple)
193fork.items.add(adventurelib_game_items.cloak)
194cave_mouth.items.add(adventurelib_game_items.slug)
195
196# Place characters where they should be
197hamlet.characters.add(adventurelib_game_characters.elder_barron)
198blacksmith_shop.characters.add(adventurelib_game_characters.blacksmith)
199wizard_hut.characters.add(adventurelib_game_characters.wizard_trent)
200giant_cave.characters.add(adventurelib_game_characters.giant)
1"""
2Items for the adventurelib Game
3"""
4
5# Import the adventurelib library
6import adventurelib as adv
7
8# All items have some basic properties
9adv.Item.color = "undistinguished"
10adv.Item.description = "a generic thing"
11adv.Item.edible = False
12adv.Item.wearable = False
13
14# Create your "flavor" items
15apple = adv.Item("small red apple", "apple")
16apple.color = "red"
17apple.description = "a small ripe red apple"
18apple.edible = True
19apple.wearable = False
20
21cloak = adv.Item("wool cloak", "cloak")
22cloak.color = "grey tweed"
23cloak.description = (
24 "a grey tweed cloak, heavy enough to keep the wind and rain at bay"
25)
26cloak.edible = False
27cloak.wearable = True
28
29slug = adv.Item("slimy brown slug", "slug")
30slug.color = "slimy brown"
31slug.description = "a fat, slimy, brown slug"
32slug.edible = True
33slug.wearable = False
34
35# Create the real items you need
36wooden_sword = adv.Item("wooden sword", "sword")
37wooden_sword.color = "brown"
38wooden_sword.description = (
39 "a small wooden practice sword, not even sharp enough to cut milk"
40)
41wooden_sword.edible = False
42wooden_sword.wearable = False
43wooden_sword.damage = 4
44wooden_sword.bonus = 0
45
46steel_sword = adv.Item("steel sword", "sword")
47steel_sword.color = "steely grey"
48steel_sword.description = (
49 "a finely made steel sword, honed to a razor edge, ready for blood"
50)
51steel_sword.edible = False
52steel_sword.wearable = False
53steel_sword.damage = 10
54steel_sword.bonus = 0
1"""
2Characters for the adventurelib Game
3"""
4
5# Import the adventurelib library
6import adventurelib as adv
7
8# All characters have some properties
9adv.Item.greeting = ""
10adv.Item.context = ""
11
12# Your characters
13elder_barron = adv.Item("Elder Barron", "elder", "barron")
14elder_barron.description = """Elder Barron, a tall distinguished member
15of the community. His steely grey hair and stiff beard inspire confidence."""
16elder_barron.greeting = (
17 "I have some information for you. Would you like to hear it?"
18)
19elder_barron.context = "elder"
20
21blacksmith = adv.Item("Alanna Smith", "Alanna", "blacksmith", "smith")
22blacksmith.description = """Alanna the blacksmith stands just 1.5m tall,
23and her strength lies in her arms and heart"""
24blacksmith.greeting = (
25 "Oh, hi! I've got some stuff for sale. Do you want to see it?"
26)
27blacksmith.context = "blacksmith"
28
29wizard_trent = adv.Item("Trent the Wizard", "Trent", "wizard")
30wizard_trent.description = """Trent's wizardly studies have apparently
31aged him past his years, but they have also preserved his life longer than
32expected."""
33wizard_trent.greeting = (
34 "It's been a long time since I've had a visitor? Do you seek wisdom?"
35)
36wizard_trent.context = "wizard"
37
38giant = adv.Item("hungry giant", "giant")
39giant.description = """Almost four meters of hulking brutish strength
40stands before you, his breath rank with rotten meat, his mangy hair
41tangled and matted"""
42giant.greeting = "Argh! Who dares invade my home? Prepare to defend yourself!"
43giant.context = "giant"
You can start this game with the following command:
(venv) $ python adventurelib/adventurelib_game.py
After defining the backstory, you mapped out the different game areas and the paths which the player uses move between them:
Each area has various properties associated with it, including:
- Items and characters that are in that area
- Some exits that are locked
- A title, a short description, and a longer description
- An indication of whether the player has been in this area or not
To ensure that each area has its own instance of each of these properties, you create a subclass of Room
called GameArea
in adventurelib_game_rooms.py
on lines 15 to 41. Items in each room are held in a Bag
object called items
, while characters are stored in characters
, defined on lines 28 and 31. Now you can create GameArea
objects, describe them, and populate them with unique items and characters, which are all imported on lines 9 and 12.
Some game items are required to finish the game, while others are just there for flavor and to add interest. Flavor items are identified and placed on lines 192 to 194, followed by characters on lines 197 to 200.
All of your game items are defined in adventurelib_game_items.py
as objects of type Item()
. Game items have properties that define them, but because you’ve used the Item
base class, some basic universal properties are added to the class on lines 9 to 12. These properties are used when the item is created. For example, the apple
object is created on lines 15 to 19 and defines each of the universal properties when it is created.
Some items, however, have specific properties unique to the item. For example, the wooden_sword
and steel_sword
items need properties to track the damage they do and any magical bonuses they carry. Those are added on lines 43 to 44 and 53 to 54.
Interacting with characters helps drive the game story forward and often gives the player a reason to explore. Characters in adventurelib are created as Item
objects, and for this game are defined in adventurelib_game_characters.py
.
Each character, like each item, has universal properties associated with it, such as a long description and the greeting used when the player encounters it for the first time. These properties are declared on lines 9 and 10, and they’re defined for each character when the character is created.
Of course, if you have characters, then it makes sense for the player to talk to and interact with them. It’s often a good idea to know when you’re interacting with a character, and when you’re just in the same game area with one.
This is done by using an adventurelib concept called a context. Contexts allow you to turn on different commands for different situations. They also allow certain commands to behave differently, and they track additional information about actions that the player may have taken.
When the game starts, there’s no context set. As the player progresses, they first encounter Elder Barron. When the player types "talk to elder"
, the context is set to elder.context
, which in this case is elder
.
Elder Barron’s greeting ends with a yes or no question to the player. If the player types "yes"
, then the command handler on line 173 in adventurelib_game.py
is triggered, which is defined as @when("yes", context="elder")
, as shown below:
Later, when the player is talking to the blacksmith, a second level of context is added to reflect that they’re engaged in a possible weapon trade. Lines 203 to 233 define the discussion with the blacksmith, which includes the offer of a weapons trade. A new context is defined on line 222, which allows the same "yes"
command to be used elegantly in multiple ways.
You can also check the context in a command handler. For example, the player cannot simply leave the fight with the giant by ending the conversation. The "goodbye"
command handler defined on lines 389 to 403 checks if the player is in the "giant"
context, which is entered when they start fighting the giant. If so, they’re not allowed to stop the conversation — it’s a fight to the death!
You can also ask questions of the player requiring a specific answer. When the player talks to the wizard Trent, they’re asked to solve a riddle. An incorrect answer will end the interaction. While the correct answer is handled with the command handler on lines 252 to 262, one of the nearly infinite wrong answers won’t match any handler.
Commands with no matches are handled by the no_command_matches()
function on lines 497 to 503. You leverage this to handle incorrect answers to the wizard’s riddle by checking for the wizard.riddle
context on line 498. Any incorrect answer to the riddle will result in the wizard ending the conversation. You connect this to adventurelib on line 523 by setting adventurelib.no_command_matches
to your new function.
You can customize the prompt shown to the player by writing a function that returns the new prompt. Your new prompt is defined on lines 483 to 495 and connected to adventurelib on line 520.
Of course, there’s always more that you could add. Creating a complete text adventure game is challenging, and adventurelib makes sure the the main challenge lies in painting a picture with words.
Ren’Py
The modern descendant of the pure text adventure is the visual novel, which highlights the storytelling aspect of the game, limiting player interactions while adding visuals and sound to heighten the experience. Visual novels are the graphic novels of the game world — modern, innovative, and extremely compelling to create and consume.
Ren’Py is a tool based on Pygame and designed specifically to create visual novels. Ren’Py takes its name from the Japanese word for romantic love and provides tools and a framework for crafting compelling visual novels.
To be fair, Ren’Py is not strictly a Python library that you can pip install
and use. Ren’Py games are created using the Ren’Py Launcher, which comes with the full Ren’Py SDK. This launcher also features a game editor, although you can edit your game in your editor of choice. Ren’Py also features its own scripting language for game creation. However, Ren’Py is based on Pygame, and it’s extendable using Python, which warrants its appearance here.
Ren’Py Installation
As mentioned, Ren’Py requires not only the SDK, but also the Ren’Py Launcher. These are packaged together in a single unit, which you need to download.
Knowing which package to download and how to install it depends on your platform. Ren’Py provides installers and instructions for Windows, macOS, and Linux users:
- Windows
- Linux
- macOS
Windows users should download the provided executable, then run it to install the SDK and the Ren’Py Launcher.
Linux users should download the provided tarball to a convenient location, then expand it using bunzip2
.
macOS users should download the DMG file provided, double-click the file to open it, and copy the contents to a convenient location.
After the package is installed, you can navigate to the folder containing the SDK then run the Ren’Py Launcher. Windows users should use renpy.exe
, while macOS and Linux users should run renpy.sh
. This will start the Ren’Py Launcher for the first time:
This is where you’ll start new Ren’Py projects, work on existing projects, and set overall preferences for Ren’Py.
Basic Concepts
Ren’Py games start as new projects in the Ren’Py Launcher. Creating one will set up the proper file and folder structure for a Ren’Py game. After the project is set up, you can use your own editor to write your game, although the Ren’Py Launcher is required to run the game:
Ren’Py games are contained in files called scripts. Don’t think of Ren’Py scripts as you would shell scripts. They’re more analogous to scripts for plays or television shows. Ren’Py scripts have the extension .rpy
and are written in the Ren’Py language. Your game can consist of as many scripts as you like, which are all stored in the game/
subfolder of your project folder.
When you create a new Ren’Py project, the following scripts are created for you to use and update:
gui.rpy
, which defines the look of all UI elements used in your gameoptions.rpy
, which defines changeable options to customize your gamescreens.rpy
, which defines the styles used for dialogue, menus, and other game outputscript.rpy
, which is where you start writing your game
To run the sample games from the downloaded materials for this tutorial, you’ll use the following process:
- Start the Ren’Py Launcher.
- Click Preferences, then Projects Directory.
- Change the Projects Directory to the
renpy
folder in the repository that you downloaded. - Click Return to return to the main Ren’Py Launcher page.
You’ll see basic_game
and giant_quest_game
in the Projects list on the left. Select the one that you wish to run, then click Launch Project.
For this example, you’ll only modify the script.rpy
file for basic_game
. The complete code for this game can be found in the downloaded materials, as well as below:
1# The script of the game goes in this file.
2
3# Declare characters used by this game. The color argument colorizes the
4# name of the character.
5
6define kevin = Character("Kevin", color="#c8ffc8")
7define mom = Character("Mom", color="#c8ffff")
8define me = Character("Me", color="#c8c8ff")
9
10# The game starts here.
11
12label start:
13
14 # Some basic narration to start the game
15
16 "You hear your alarm going off, and your mother calling to you."
17
18 mom "It's time to wake up. If I can hear your alarm,
19 you can hear it to - let's go!"
20
21 "Reluctantly you open your eyes."
22
23 # Show a background.
24
25 scene bedroom day
26
27 # This shows the basic narration
28
29 "You awaken in your bedroom after a good night's rest.
30 Laying there sleepily, your eyes wander to the clock on your phone."
31
32 me "Yoinks! I'm gonna be late!"
33
34 "You leap out of bed and quickly put on some clothes.
35 Grabbing your book bag, you sprint for the door to the living room."
36
37 scene hallway day
38
39 "Your brother is waiting for you in the hall."
40
41 show kevin normal
42
43 kevin "Let's go, loser! We're gonna be late!"
44
45 mom "Got everything, honey?"
46
47 menu:
48 "Yes, I've got everything.":
49 jump follow_kevin
50
51 "Wait, I forgot my phone!":
52 jump check_room
53
54label check_room:
55
56 me "Wait! My phone!"
57
58 kevin "Whatever. See you outside!"
59
60 "You sprint back to your room to get your phone."
61
62 scene bedroom day
63
64 "You grab the phone from the nightstand and sprint back to the hall."
65
66 scene hallway day
67
68 "True to his word, Kevin is already outside."
69
70 jump outside
71
72label follow_kevin:
73
74 kevin "Then let's go!"
75
76 "You follow Kevin out to the street."
77
78label outside:
79
80 scene street
81
82 show kevin normal
83
84 kevin "About time you got here. Let's Go!"
85
86 # This ends the game
87 return
Labels define entry points into your story, and are often used to start new scenes and provide alternate paths through the story. All Ren’Py games start running at the line label start:
, which can appear in any script you choose. You can see this on line 12 of script.rpy
.
You can also use labels to define background images, set up transitions between scenes, and control the appearance of characters. In the sample, a second scene starts on line 54 with the line label check_room:
.
Text enclosed in double-quotes on a line is called a say statement. A single string on a line is treated as narration. Two strings get treated as dialogue, identifying a character first and then providing the line that they’re speaking.
At the beginning of the game, narration is seen on line 16, which sets the scene. Dialogue is provided on line 18, when your mom calls to you.
You can define characters by simply naming them in the story. However, you can also define characters at the top of your script. You can see this on lines 6 to 8, where you, your brother Kevin, and your mom are defined. The define
statement initializes the three variables as Characters
, giving them a display name, followed by a text color used to display the name.
Of course, this is a visual novel, so it makes sense that Ren’Py would have a way to handle images. Like Pygame Zero, Ren’Py requires that all images and sounds used in the game reside in specific folders. Images are found in the game/images/
folder, and sounds are in the game/audio/
folder. In the game script, you refer to them by filename without any file extension.
Line 25 shows this in action, when you open your eyes and see your bedroom for the first time. The scene
keyword clears the screen, then displays the bedroom day.png
image. Ren’Py supports the JPG, WEBP, and PNG image formats.
You can also display characters on the screen using the show
keyword and the same naming convention for the image. Line 41 shows a picture of your brother Kevin, stored as kevin normal.png
.
Of course, it’s not much of a game if you can’t make decisions to affect the outcome. In Ren’Py, players make choices from a menu presented to them in the course of the game. The game reacts by jumping to predefined labels, changing character images, playing sounds, or taking other actions as necessary.
A basic choice in this sample is shown on lines 47 to 52, where you realize you’ve forgotten your phone. In a more complete story, this choice may have consequences later.
Of course, you can do much more with Ren’Py. You can control transitions between scenes, have characters enter and leave scenes in specific ways, and include sound and music for your game. Ren’Py also supports writing more complex Python code, including using Python data types and making direct Python function calls. Now take a closer look at these capabilities in a more advanced application.
Advanced Application
To show the depth of Ren’Py, you’ll implement the same game as you did for adventurelib. As a reminder, here’s the basic design of that game:
- You live in a small, quiet hamlet.
- Recently, your neighbors have begun complaining of missing livestock.
- As a member of a night patrol, you notice a broken fence and a trail leading away from it.
- You decide to investigate, armed only with a wooden practice sword.
The game has several areas to define and provide images for. For example, you’ll need images and definitions for your small, quiet hamlet, the trail leading away from the field, a nearby village where you can buy a better weapon, a side path leading to a wizard who can enchant your weapon, and the cave containing the giant who has been taking your livestock.
There are also a few characters to define and provide images for. You need a blacksmith who can give you a better weapon, a wizard who can enchant your weapon, and the giant whom you need to defeat.
For this example, you’ll create four separate scripts:
script.rpy
, which is where the game startstown.rpy
, which contains the story of the nearby villagepath.rpy
, which contains the path between villagesgiant.rpy
, which contains the logic for the giant battle
You can create the wizard encounter as an independent exercise.
The complete code for this game can be found in the downloaded materials at renpy_sample/giant_quest/
and is also available below:
1#
2# Complete game in Ren'Py
3#
4# This game demonstrates some of the more advanced features of
5# Ren'Py, including:
6# - Multiple sprites
7# - Handling user input
8# - Selecting alternate outcomes
9# - Tracking score and inventory
10#
11
12## Declare characters used by this game. The color argument colorizes the
13## name of the character.
14define player = Character("Me", color="#c8ffff")
15define smith = Character("Miranda, village blacksmith", color="#99ff9c")
16define wizard = Character("Endeavor, cryptic wizard", color="#f4d3ff")
17define giant = Character("Maull, terrifying giant", color="#ff8c8c")
18
19## Images used in the game
20# Backgrounds
21image starting path = "BG10a_1280.jpg"
22image crossroads = "BG19a01_1280.jpg"
23
24# Items
25image wooden sword = "SwordWood.png"
26image steel sword = "Sword.png"
27image enchanted sword = "SwordT2.png"
28
29## Default settings
30# What is the current weapon?
31default current_weapon = "wooden sword"
32
33# What is the weapon damage?
34# These change when the weapon is upgraded or enchanted
35default base_damage = 4
36default multiplier = 1
37default additional = 0
38
39# Did they cross the bridge to town?
40default cross_bridge = False
41
42# You need this for the giant battle later
43
44init python:
45 from random import randint
46
47# The game starts here.
48
49label start:
50
51 # Show the initial background.
52
53 scene starting path
54 with fade
55
56 # Begin narration
57
58 "Growing up in a small hamlet was boring, but reliable and safe.
59 At least, it was until the neighbors began complaining of missing
60 livestock. That's when the evening patrols began."
61
62 "While on patrol just before dawn, your group noticed broken fence
63 around a cattle paddock. Beyond the broken fence,
64 a crude trail had been blazed to a road leading away from town."
65
66 # Show the current weapon
67 show expression current_weapon at left
68 with moveinleft
69
70 "After reporting back to the town council, it was decided that you
71 should follow the tracks to discover the fate of the livestock.
72 You picked up your only weapon, a simple wooden practice sword,
73 and set off."
74
75 scene crossroads
76 with fade
77
78 show expression current_weapon at left
79
80 "Following the path, you come to a bridge across the river."
81
82 "Crossing the bridge will take you to the county seat,
83 where you may hear some news or get supplies.
84 The tracks, however, continue straight on the path."
85
86 menu optional_name:
87 "Which direction will you travel?"
88
89 "Cross the bridge":
90 $ cross_bridge = True
91 jump town
92 "Continue on the path":
93 jump path
94
95 "Your quest is ended!"
96
97 return
1##
2## Code for the interactions in town
3##
4
5## Backgrounds
6image distant town = "4_road_a.jpg"
7image within town = "3_blacksmith_a.jpg"
8
9# Characters
10image blacksmith greeting = "blacksmith1.png"
11image blacksmith confused = "blacksmith2.png"
12image blacksmith happy = "blacksmith3.png"
13image blacksmith shocked = "blacksmith4.png"
14
15label town:
16
17 scene distant town
18 with fade
19
20 show expression current_weapon at left
21
22 "Crossing the bridge, you stride away from the river along a
23 well worn path. The way is pleasant, and you find yourself humming
24 a tune as you break into a small clearing."
25
26 "From here, you can make out the county seat of Fetheron.
27 You feel confident you can find help for your quest here."
28
29 scene within town
30 with fade
31
32 show expression current_weapon at left
33
34 "As you enter town, you immediately begin seeking the local blacksmith.
35 After asking one of the townsfolk, you find the smithy on the far
36 south end of town. You approach the smithy,
37 smelling the smoke of the furnace long before you hear
38 the pounding of hammer on steel."
39
40 player "Hello! Is the smith in?"
41
42 smith "Who wants to know?"
43
44 show blacksmith greeting
45
46 "The blacksmith appears from her bellows.
47 She greets you with a warm smile."
48
49 smith "Oh, hello! You're from the next town over, right?"
50
51 menu:
52 "Yes, from the other side of the river.":
53 show blacksmith happy
54
55 smith "I thought I recognized you. Nice to see you!"
56
57 "Look, I don't have time for pleasantries, can we get to business?":
58 show blacksmith shocked
59
60 smith "Hey, just trying to make conversation"
61
62 smith "So, what can I do for you?"
63
64 player "I need a better weapon than this wooden thing."
65
66 show blacksmith confused
67
68 smith "Are you going to be doing something dangerous?"
69
70 player "Have you heard about the missing livestock in town?"
71
72 smith "Of course. Everyone has. What do you know about it?"
73
74 player "Well, I'm tracking whatever took them from our town."
75
76 smith "Oh, I see. So you want something better to fight with!"
77
78 player "Exactly! Can you help?"
79
80 smith "I've got just the thing. Been working on it for a while,
81 but didn't know what to do with it. Now I know."
82
83 "Miranda walks back past the furnace to a small rack.
84 On it, a gleaming steel sword rests.
85 She picks it up and walks back to you."
86
87 smith "Will this do?"
88
89 menu:
90 "It's perfect!":
91 show blacksmith happy
92
93 smith "Wonderful! Give me the wooden one -
94 I can use it in the furnace!"
95
96 $ current_weapon = "steel sword"
97 $ base_damage = 6
98 $ multiplier = 2
99
100 "Is that piece of junk it?":
101 show blacksmith confused
102
103 smith "I worked on this for weeks.
104 If you don't want it, then don't take it."
105
106 # Show the current weapon
107 show expression current_weapon at left
108
109 smith "Anything else?"
110
111 player "Nope, that's all."
112
113 smith "Alright. Good luck!"
114
115 scene distant town
116 with fade
117
118 show expression current_weapon at left
119
120 "You make your way back through town.
121 Glancing back at the town, you wonder if
122 you can keep them safe too."
123
124 jump path
1##
2## Code for the interactions in town
3##
4
5## Backgrounds
6image path = "1_forest_a.jpg"
7image wizard hut = "BG600a_1280.jpg"
8
9# Characters
10image wizard greeting = "wizard1.png"
11image wizard happy = "wizard2.png"
12image wizard confused = "wizard3.png"
13image wizard shocked = "wizard4.png"
14
15label path:
16
17 scene path
18 with fade
19
20 show expression current_weapon at left
21
22 "You pick up the tracks as you follow the path through the woods."
23
24 jump giant_battle
1##
2## Code for the giant battle
3##
4
5## Backgrounds
6image forest = "forest_hill_night.jpg"
7
8# Characters
9image giant greeting = "giant1.png"
10image giant unhappy = "giant2.png"
11image giant angry = "giant3.png"
12image giant hurt = "giant4.png"
13
14# Text of the giant encounter
15label giant_battle:
16
17 scene forest
18 with fade
19
20 show expression current_weapon at left
21
22 "As you follow the tracks down the path, night falls.
23 You hear sounds in the distance:
24 cows, goats, sheep. You've found the livestock!"
25
26 show giant greeting
27
28 "As you approach the clearing and see your villages livestock,
29 a giant appears."
30
31 giant "Who are you?"
32
33 player "I've come to get our livestock back."
34
35 giant "You and which army, little ... whatever you are?"
36
37 show giant unhappy
38
39 "The giant bears down on you - the battle is joined!"
40
41python:
42
43 def show_giant_condition(giant_hp):
44 if giant_hp < 10:
45 renpy.say(None, "The giant staggers, his eyes unfocused.")
46 elif giant_hp < 20:
47 renpy.say(None, "The giant's steps become more unsteady.")
48 elif giant_hp < 30:
49 renpy.say(
50 None, "The giant sweats and wipes the blood from his brow."
51 )
52 elif giant_hp < 40:
53 renpy.say(
54 None,
55 "The giant snorts and grits his teeth against the pain.",
56 )
57 else:
58 renpy.say(
59 None,
60 "The giant smiles and readies himself for the attack.",
61 )
62
63 def show_player_condition(player_hp):
64 if player_hp < 4:
65 renpy.say(
66 None,
67 "Your eyes lose focus on the giant as you sway unsteadily.",
68 )
69 elif player_hp < 8:
70 renpy.say(
71 None,
72 "Your footing becomes less steady as you swing your sword sloppily.",
73 )
74 elif player_hp < 12:
75 renpy.say(
76 None,
77 "Blood mixes with sweat on your face as you wipe it from your eyes.",
78 )
79 elif player_hp < 16:
80 renpy.say(
81 None,
82 "You bite down as the pain begins to make itself felt.",
83 )
84 else:
85 renpy.say(None, "You charge into the fray valiantly!")
86
87 def fight_giant():
88
89 # Default values
90 giant_hp = 50
91 player_hp = 20
92 giant_damage = 4
93
94 battle_over = False
95 player_wins = False
96
97 # Keep swinging until something happens
98 while not battle_over:
99
100 renpy.say(
101 None,
102 "You have {0} hit points. Do you want to fight or flee?".format(
103 player_hp
104 ),
105 interact=False,
106 )
107 battle_over = renpy.display_menu(
108 [("Fight!", False), ("Flee!", True)]
109 )
110
111 if battle_over:
112 player_wins = False
113 break
114
115 # The player gets a swing
116 player_attack = (
117 randint(1, base_damage + 1) * multiplier + additional
118 )
119 renpy.say(
120 None,
121 "You swing your {0}, doing {1} damage!".format(
122 current_weapon, player_attack
123 ),
124 )
125 giant_hp -= player_attack
126
127 # Is the giant dead?
128 if giant_hp <= 0:
129 battle_over = True
130 player_wins = True
131 break
132
133 show_giant_condition(giant_hp)
134
135 # Then the giant tries
136 giant_attack = randint(0, giant_damage)
137 if giant_attack == 0:
138 renpy.say(
139 None,
140 "The giant's arm whistles harmlessly over your head!",
141 )
142 else:
143 renpy.say(
144 None,
145 "The giant swings his mighty fist, and does {0} damage!".format(
146 giant_attack
147 ),
148 )
149 player_hp -= giant_attack
150
151 # Is the player dead?
152 if player_hp <= 0:
153 battle_over = True
154 player_wins = False
155
156 show_player_condition(player_hp)
157
158 # Return who died
159 return player_wins
160
161 # fight_giant returns True if the player wins.
162 if fight_giant():
163 renpy.jump("player_wins")
164 else:
165 renpy.jump("giant_wins")
166
167label player_wins:
168
169 "The giant's eyes glaze over as he falls heavily to the ground.
170 The earth shakes as his bulk lands face down,
171 and his death rattle fills the air."
172
173 hide giant
174
175 "You are victorious! The land is safe from the giant!"
176
177 return
178
179label giant_wins:
180
181 "The giant takes one last swing, knocking you down.
182 Your vision clouds, and you see the ground rising to meet you.
183 As you slowly lose consciousness, your last vision is
184 the smiling figure of the giant as he advances on you."
185
186 "You have lost!"
187
188 return
As in the previous example, you define Character()
objects before the script begins on lines 14 to 17 of script.rpy
.
You can also define background or character image
objects for later use. Lines 21 to 27 define several images which you refer to later, both to use as backgrounds and to display as items. Using this syntax allows you to assign shorter and more descriptive internal names for images. Later, you’ll see how they’re displayed.
You also need to track the capabilities of the equipped weapon. This is done on lines 31 to 37, using default
variable values, which you’ll use during the giant battle later.
To indicate which weapon is enabled, you show the image as an expression. Ren’Py expressions are small images displayed in the corners of the game window and are used to show a wide variety of information. For this game, you use an expression to show the weapon using show expression
first on lines 67 and 68.
The show
command has a number of modifiers documented. The with moveinleft
modifier causes the current_weapon
image to slide onto the screen from the left. Also, it’s important to remember that every time scene
changes, the entire screen is cleared, requiring you to show the current weapon again. You can see that on lines 75 to 78.
When you enter the town in town.rpy
, you meet the blacksmith, who greets you:
The blacksmith offers you the opportunity to upgrade your weapon. If you choose to do so, then you update the values for current_weapon
and the weapon stats. This is done on lines 93 to 98.
Lines that begin with the $
character are interpreted by Ren’Py as Python statements, allowing you to write arbitrary Python code as necessary. Updating the current_weapon
and weapon stats is done using three Python statements on lines 96 to 98, which change the values of the the default
variables that you defined at the top of script.rpy
.
You can also define a large block of Python code using a python:
section, as shown in giant.rpy
starting on line 41.
Lines 43 to 61 contain a helper function to show the condition of the giant, based on the giant’s remaining hit points. It uses the renpy.say()
method to output narration back to the main Ren’Py window. A similar helper function to show the player’s condition is seen on lines 63 to 85.
The battle is controlled by fight_giant()
on lines 87 to 159. The game loop is implemented on line 98 and is controlled by the battle_over
variable. Player choices of fight or flee are displayed using the renpy.display_menu()
method.
If the player fights, then a random amount of damage is done on lines 116 to 118, and the giant’s hit points are adjusted. If the giant is still alive, then they get to attack in a similar fashion on lines 136 to 149. Note that the giant has a chance to miss, while the player always hits. The fight continues either until the player or the giant has zero hit points or until the player flees:
It’s important note that this code is very similar to the code that you used in the adventurelib battle. This shows how you can drop full Python code into your Ren’Py games without needing to translate it into Ren’Py script.
There’s much more to Ren’Py than what you’ve tried out here. Consult the Ren’Py documentation for more complete details.
Other Notable Python Game Engines
These five engines are only a small sampling of the many different Python game engines available. There are dozens of others available, and a few are worth noting here:
-
Wasabi 2D is developed by the team behind Pygame Zero. It’s a modern framework built on moderngl that automates rendering, provides coroutines for animation effects, has built-in particle effects, and uses an event-driven model for game play.
-
cocos2d is a framework designed for coding cross-platform games. Sadly, cocos2d-python hasn’t been updated since 2017.
-
Panda 3D is an open-source framework for creating 3D games and 3D renderings. Panda 3D is portable across platforms, supports multiple asset types, connects out of the box with numerous third-party libraries, and provides built-in pipeline profiling.
-
Ursina is built on top of Panda 3D and provides a dedicated game development engine that simplifies many aspects of Panda 3D. Well supported and well documented, Ursina is under active development at the time of this writing.
-
PursuedPyBear is billed as an educational library. It boasts a scene management system, frame-based animated sprites which can be paused, and a low barrier to entry. Documentation is sparse, but help is only a GitHub discussion away.
New Python game engines are created every day. If you find one that suits your needs and wasn’t mentioned here, please sing its praises in the comments!
Sources for Game Assets
Often, creating game assets is the biggest issue facing game authors. Large video game companies employ teams of artists, animators, and musicians to design the look and sound of their games. Solo game developers with a background in coding may find this aspect of game development daunting. Luckily, there are many different sources for game assets available. Here are some that were vital in locating assets for the games in this tutorial:
-
OpenGameArt.org hosts a wide variety of game art, music, backgrounds, icons, and other assets for both 2D and 3D games. Artists and musicians list their assets for download, which you can download and use in your games. Most assets are freely available, and licensing terms may apply to many of them.
-
Kenney.nl hosts a set of free and paid assets, many of which can be found nowhere else. Donations are always welcome to support the free assets, which are all licensed for use in commercial games.
-
Itch.io is a marketplace for digital creators focused on independent game development. Here you can find digital assets for just about any purpose, both free and paid, along with complete games. Individual creators control their own content here, so you’re always working directly with talented individuals.
Most assets available from third-parties carry licensing terms dictating the proper and allowable use of the assets. As the user of these assets, it’s your responsibility to read, understand, and comply with the licensing terms as defined by the asset owner. If you have questions or concerns about those terms, please consult a legal professional for assistance.
All of the assets used in the games referenced in this article conform to their respective licensing requirements.
Conclusion
Congratulations, great game design is now within your reach! Thanks to Python and a buffet of highly capable Python game engines, you can create quality computer games much more easily than before. In this tutorial, you’ve explored several such game engines, learning the information that you need to start crafting your own Python video games!
By now, you’ve seen some of the top Python game engines in action, and you’ve:
- Explored the pros and cons of several popular Python game engines
- Experienced how they compare to stand-alone game engines
- Learned about other Python game engines available
If you’d like to review the code for the games in this tutorial, you can do so by clicking the link below:
Now you can choose the best Python game engine for your purpose. So what are you waiting for? Get out there and write some games!
Написать игровой движок на первом курсе: легко! (ну почти) +22
Программирование, Учебный процесс в IT, Дизайн игр, C++, Блог компании Питерская Вышка
Рекомендация: подборка платных и бесплатных курсов Java — https://katalog-kursov.ru/
Привет! Меня зовут Глеб Марьин, я учусь на первом курсе бакалавриата «Прикладная математика и информатика» в Питерской Вышке. Во втором семестре все первокурсники нашей программы делают командные проекты по С++. Мы с моими партнерами по команде решили написать игровой движок.
О том, что у нас получается, читайте под катом.
Всего нас в команде трое: я, Алексей Лучинин и Илья Онофрийчук. Никто из нас не является экспертом в разработке игр, а тем более в создании игровых движков. Для нас это первый большой проект: до него мы выполняли только домашние задания и лабораторные работы, так что едва ли профессионалы в области компьютерной графики найдут здесь новую для себя информацию. Мы будем рады, если наши идеи помогут тем, кто тоже хочет создать свой движок. Но тема эта сложна и многогранна, и статья ни в коем случае не претендует на полноту специализированной литературы.
Всем остальным, кому интересно узнать о нашей реализации, — приятного чтения!
Графика
Первое окно, мышь и клавиатура
Для создания окон, обработки ввода с мыши и клавиатуры мы выбрали библиотеку SDL2. Это был случайный выбор, но мы о нем пока что не пожалели.
Важно было на самом первом этапе написать удобную обертку над библиотекой, чтобы можно было парой строчек создавать окно, проделывать с ним манипуляции вроде перемещения курсора и входа в полноэкранный режим и обрабатывать события: нажатия клавиш, перемещения курсора. Задача оказалось несложной: мы быстро сделали программу, которая умеет закрывать и открывать окно, а при нажатии на ПКМ выводить «Hello, World!».
Тут появился главный игровой цикл:
Event ev;
bool running = true;
while (running):
ev = pullEvent();
for handler in handlers[ev.type]:
handler.handleEvent(ev);
К каждому событию привязаны обработчики — handlers
, например, handlers[QUIT] = {QuitHandler()}
. Их задача — обрабатывать соответствующее событие. QuitHandler
в примере будет выставлять running = false
, тем самым останавливая игру.
Hello World
Для рисования в движке мы используем OpenGL
. Первым Hello World
у нас, как, думаю, и во многих проектах, был белый квадрат на черном фоне:
glBegin(GL_QUADS);
glVertex2f(-1.0f, 1.0f);
glVertex2f(1.0f, 1.0f);
glVertex2f(1.0f, -1.0f);
glVertex2f(-1.0f, -1.0f);
glEnd();
Затем мы научились рисовать двумерный многоугольник и вынесли фигуры в отдельный класс GraphicalObject2d
, который умеет поворачиваться с помощью glRotate
, перемещаться с glTranslate
и растягиваться с glScale
. Цвет мы задаем по четырем каналам, используя glColor4f(r, g, b, a)
.
С таким функционалом уже можно сделать красивый фонтан из квадратиков. Создадим класс ParticleSystem
, у которого есть массив объектов. Каждую итерацию главного цикла система частиц обновляет старые квадратики и собирает сколько-то новых, которые пускает в случайном направлении:
Камера
Следующим шагом нужно было написать камеру, которая могла бы перемещаться и смотреть в разные стороны. Чтобы понять, как решить эту задачу, нам потребовались знания из линейной алгебры. Если вам это не очень интересно, можете пропустить раздел, посмотреть гифку и читать дальше.
Мы хотим нарисовать в координатах экрана вершину, зная ее координаты относительно центра объекта, которому она принадлежит.
- Сначала нам понадобится найти ее координаты относительно центра мира, в котором находится объект.
- Затем, зная координаты и расположение камеры, найти положение вершины в базисе камеры.
- После чего спроецировать вершину на плоскость экрана.
Как можно видеть, выделяются три этапа. Им соответствуют домножения на три матрицы. Мы назвали эти матрицы Model
, View
и Projection
.
Начнем с получения координат объекта в базисе мира. С объектом можно делать три преобразования: масштабировать, поворачивать и перемещать. Все эти операции задаются домножением исходного вектора (координат в базисе объекта) на соответствующие матрицы. Тогда матрица Model
будет выглядеть так:
Model = Translate * Scale * Rotate.
Дальше, зная положение камеры, мы хотим определить координаты в ее базисе: домножить полученные ранее координаты на матрицу View
. В C++ это удобно вычислить с помощью функции:
glm::mat4 View = glm::lookAt(cameraPosition, objectPosition, up);
Дословно: посмотри на objectPosition
с позиции cameraPosition
, причем направление вверх — это «up». Зачем нужно это направление? Представьте, что вы фотографируете чайник. Вы направляете на него камеру и располагаете чайник в кадре. В этот момент вы можете точно сказать, где у кадра верх (скорее всего там, где у чайника крышка). Программа не может за нас додумать, как расположить кадр, и именно поэтому нужно указывать вектор «up».
Мы получили координаты в базисе камеры, осталось спроецировать полученные координаты на плоскость камеры. Этим занимается матрица Projection
, которая создает эффект уменьшения объекта при его отдалении от нас.
Чтобы получить координаты вершины на экране, нужно перемножить вектор на матрицу по крайней мере пять раз. Все матрицы имеют размер 4 на 4, так что придется проделать довольно много операций умножения. Мы не хотим нагружать ядра процессора большим количеством простых задач. Для этого лучше подойдет видеокарта, у которой есть необходимые ресурсы. Значит, нужно написать шейдер: небольшую инструкцию для видеокарты. В OpenGL есть специальный шейдерный язык GLSL, похожий на C, который поможет нам это сделать. Не будем вдаваться в подробности написания шейдера, лучше наконец-то посмотрим на то, что вышло:
Пояснение: есть десять квадратов, которые находятся на небольшой дистанции друг за другом. По правую сторону от них находится игрок, который вращает и перемещает камеру.
Физика
Какая же игра без физики? Для обработки физического взаимодействия мы решили использовать библиотеку Box2d и создали класс WorldObject2d
, который наследовался от GraphicalObject2d
. К сожалению, не получилось использовать Box2d «из коробки», поэтому отважный Илья написал обертку к b2Body и всем физическим соединениям, которые есть в этой библиотеке.
До этого момента мы думали сделать графику в движке абсолютно двумерной, а для освещения, если решим его добавлять, использовать технику raycasting. Но у нас под рукой была замечательная камера, которая умеет отображать объекты во всех трех измерениях. Поэтому мы добавили всем двумерным объектам толщину — почему бы и нет? К тому же в перспективе это позволит делать довольно красивое освещение, которое будет оставлять тени от толстых объектов.
Освещение появилось между делом. Для его создания потребовалось написать соответствующие инструкции для рисования каждого пикселя — фрагментный шейдер.
Текстуры
Для загрузки изображений мы использовали библиотеку DevIL. Каждому GraphicalObject2d
стал соответствовать один экземпляр класса GraphicalPolygon
— лицевая часть объекта — и GraphicalEdge
— боковая часть. На каждую можно натянуть свою текстуру. Первый результат:
Все основное, что требуется от графики, уже готово: отрисовка, один источник освещения и текстуры. Графика — на данном этапе все.
Машина состояний, задание поведений объектов
Каждый объект, каким бы он ни был, — состоянием в машине состояний, графическим или же физическим — должен «тикать», то есть обновляться каждую итерацию игрового цикла.
Объекты, которые умеют обновляться, наследуются от созданного нами класса Behavior. У него есть функции onStart, onActive, onStop
, которые позволяют переопределить поведение наследника при запуске, при жизни и при завершении его активности. Теперь нужно создать верховный объект Activity
, который бы вызывал эти функции от всех объектов. Функция loop, которая это делает, выглядит следующим образом:
void loop():
onAwake();
awake = true;
while (awake):
onStart();
running = true
while (running):
onActive();
onStop();
onDestroy();
Пока running == true
, кто-нибудь может вызвать функцию pause()
, которая сделает running = false
. Если же кто-то вызовет kill()
, то awake
и running
обратятся в false
, и активность остановится полностью.
Проблема: хотим поставить группу объектов на паузу, например, систему частиц и частицы внутри нее. В текущем состоянии для этого нужно вручную вызвать onPause
для каждого объекта, что не очень удобно.
Решение: у каждого Behavior
пусть будет массив subBehaviors
, которые он будет обновлять, то есть:
void onStart():
onStart() // не забыть стартовать себя самого
for sb in subBehaviors:
sb.onStart() // а только после этого все дочерние Behavior
void onActive():
onActive()
for sb in subBehaviors:
sb.onActive()
И так далее, для каждой функции.
Но не любое поведение можно задать таким образом. Например, если по платформе гуляет враг — enemy, то у него, скорее всего, есть разные состояния: он стоит — idle_stay
, он гуляет по платформе, не замечая нас — idle_walk
, и в любой момент может заметить нас и перейти в состояние атаки — attack
. Еще хочется удобным образом задавать условия перехода между состояниями, например:
bool isTransitionActivated(): // для idle_walk->attack
return canSee(enemy);
Нужным паттерном является машина состояний. Ее мы тоже сделали наследником Behavior
, так как на каждом тике нужно проверять, пришло ли время переключить состояние. Это полезно не только для объектов в игре. Например, Level
— это состояние Level Switcher
, а переходы внутри машины контроллера — это условия на переключения уровней в игре.
У состояния есть три стадии: оно началось, оно тикает, оно остановлено. К каждой из стадий можно добавлять какие-то действия, например, прикрепить к объекту текстуру, применить к нему импульс, установить скорость и так далее.
Сохранения
Создавая уровень в редакторе, хочется иметь возможность сохранить его, а сама игра должна уметь загружать уровень из сохраненных данных. Поэтому все объекты, которые нужно сохранять, наследуются от класса NamedStoredObject
. Он хранит строку с именем, названием класса и обладает функцией dump()
, которая сбрасывает данные об объекте в строку.
Чтобы сделать сохранение, остается просто переопределить dump()
для каждого объекта. Загрузка — это конструктор от строки, содержащей всю информацию об объекте. Загрузка завершена, когда такой конструктор сделан для каждого объекта.
На самом деле, игра и редактор — это почти один и тот же класс, только в игре уровень загружается в режиме чтения, а в редакторе — в режиме записи. Для записи и чтения объектов из json-а движок использует библиотеку rapidjson.
Графический интерфейс
В какой-то момент перед нами встал вопрос: пусть уже написана графика, машина состояний и все прочее. Как пользователь сможет написать игру, используя это?
В первоначальном варианте ему пришлось бы отнаследоваться от Game2d
и переопределить onActive
, а в полях класса создавать объекты. Но во время создания он не может видеть того, что создает, и нужно было бы еще и скомпилировать его программу и прилинковать к нашей библиотеке. Ужас! Были бы и плюсы — можно было бы задавать столь сложные поведения, на какие только хватило бы фантазии: например, передвинуть блок земли на столько, сколько жизней у игрока, и делать это при условии, что Уран в созвездии Тельца, а курс евро не превышает 40 рублей. Однако мы все-таки решили сделать графический интерфейс.
В графическом интерфейсе количество действий, которые можно произвести с объектом, будет ограничено: перелистнуть слайд анимации, применить силу, установить определенную скорость и так далее. Та же ситуация с переходами в машине состояний. В больших движках проблему ограниченного количества действий решают связыванием текущей программы с другой — например, в Unity и Godot используется связывание с C#. Уже из этого скрипта можно будет сделать что угодно: и посмотреть, в каком созвездии Уран, и какой сейчас курс евро. У нас такой функциональности на данный момент нет, но в наши планы входит связать движок с Python 3.
Для реализации графического интерфейса мы решили использовать Dear ImGui, потому что она очень маленькая (по сравнению с широко известным Qt) и писать на ней очень просто. ImGui — парадигма создания графического интерфейса. В ней каждую итерацию главного цикла все виджеты и окна отрисовываются заново только если это нужно. С одной стороны, это уменьшает объем потребляемой памяти, но с другой, скорее всего, занимает больше времени, чем одно выполнение сложной функции создания и сохранение нужной информации для последующего рисования. Тут уже осталось только реализовать интерфейсы для создания и редактирования.
Вот как в момент выхода статьи выглядит графический интерфейс:
Редактор уровня
Редактор машины состояний
Заключение
Мы создали только основу, на которую можно вешать что-то более интересное. Иными словами, есть куда расти: можно реализовать отрисовку теней, возможность создания более чем одного источника освещения, можно связать движок с интерпретатором Python 3, чтобы писать скрипты для игры. Хотелось бы доработать интерфейс: сделать его красивее, добавить больше различных объектов, поддержку горячих клавиш…
Работы еще предстоит много, но мы довольны тем, что имеем на данный момент.
За время создания проекта мы получили много разнообразного опыта: работы с графикой, создания графических интерфейсов, работы с json файлами, обертки многочисленных C библиотек. А еще опыт написания первого большого проекта в команде. Надеемся, что нам удалось рассказать о нем так же интересно, как было интересно им заниматься
Ссылка на гихаб проекта: github.com/Glebanister/ample
Такое понятие, как игровой движок на Python, пришло к нам относительно недавно. Более популярными до какого-то времени были игровые движки на следующих языках программирования:
С++;
С#;
Lua;
JavaScript;
и др.
Но сегодня мы как раз рассмотрим, что такое игровой 2D и 3D движок на Python.
Какой выбрать игровой движок на Python
Питон сам по себе довольно популярный язык программирования. Этот скриптовый язык во многом обходит своих конкурентов. А сферы его применения настолько широки, что в двух словах и не напишешь. Поэтому разработать на нем игру — не очень сложная задача, если в арсенале есть небольшой опыт. А всю остальную информацию легко можно вытащить из сети. Ведь по Python очень много мануалов, форумов, книг, сайтов, сообществ, курсов, в том числе и по разработке игр на этом языке.
На нем можно создавать 2D и 3D игры, зависит от того, какую библиотеку и какой движок будете использовать.
Для основы по разработке игр на Питоне можно рассмотреть следующие библиотеки:
Allegro;
PySDL;
PySFML;
PyOrge.
Для старта их будет достаточно. А теперь давайте выберем игровой движок на Python. Их можно разделить на 2 основные группы:
Для разработки 2D-игр.
Для разработки 3D-игр.
Игровой движок на Python для 2D—игр
Для 2D-игр можно попробовать использовать что-то из самых популярных движков и наборов инструментов, например:
Ignifuga Game Engine;
FiFe;
Cocos2D;
Ren Py;
Python Arcade Library;
Pyglet;
Pygame;
и др.
Все они несильно отличаются друг от друга по функциональности, поэтому нужно пробовать, чтобы найти свой. Самым простым по своему использованию можно выделить Ren Py, его негласно еще называют «конструктором начинающих игроделов». Все потому, что он обладает уникальной «легкостью» в создании игр и не требует глубоких знаний самого Питона, а потому основной базы и понимания, как работает этот язык, будет достаточно, чтобы запустить свою игру на этом движке. Данный игровой движок на Python заточен под жанр «визуальная новелла». Поэтому игры в этом жанре будут получаться на нем превосходно. Сегодня в Ростове-на-Дону работает несколько видов ночных фей. И если мужчина брезгует услугами трассовых шлюх, а до элитных красоток из салонов эротического массажа он не дорос, можно выбрать средний вариант при цене 2000-3000 руб. за час. Такие проститутки на деле оказываются не менее активными и инициативными, чем премиум-феи. У них есть немалый список возможных программ, а если повезет – то удастся насладиться заодно и неплохим массажем или стриптизом. Выбирайте анкету шлюхи из Ростова на сайте и успейте позвонить сегодня первым!
Игровой движок на Python для 3D—игр
Для 3D—проектов можно присмотреться к следующим популярным решениям:
Delta3D Engine;
UPBGE;
Blender Game Engine;
Panda 3D SDK;
Godot;
и др.
Многие из перечисленных инструментов не зависят от операционной системы, и вы легко сможете установить их как на Windows, так и на Linux и MacOS.
Большой плюс разработки игр на Питоне — это лаконичный и простой код, плюс высокая скорость создания прототипа игры, что не позволяют делать другие языки при создании игр. А со временем вы научитесь интегрировать Питон и C#, тогда вполне реально сможете создать собственный игровой шедевр, так как будете способны наладить симбиоз между Python и Unity. А Юнити, как известно, один из самых популярных игровых движков в мире с большим сообществом последователей.
В общем,неважно, какой будет ваша первая игра, консольной или текстовой, — самое время попробовать сделать ее на Python, и для этого вы можете использовать любой игровой движок из перечисленных в этой статье. Ведь Питон — это один из самых простых, но в то же время очень функциональных языков программирования.
I have started developing a raycasting game in Python (using PyGame) as a learning exercise and to get a better understanding of the math and techniques involved.
Raycasting is a graphic technique used to render pseudo-3D graphics based on a 2D game world. The best-known example of a raycasting engine used in a computer game is probably Wolfenstein 3D, developed by id Software in 1992.
So firstly, here are some resources I used to upskill and get my head around the topic:
YouTube tutorial series by Standalone Coder. These videos are in Russian, but the YouTube subtitles do a good enough job to follow along.
YouTube tutorial series by Code Monkey King.
Lode’s Computer Graphics Tutorial.
Lastly, I recommend the book Game Engine Black Book: Wolfenstein 3D by Fabien Sanglard, it is not an easy read, but it gives excellent insight into the development of Wolfenstein 3D and a great deal of information into the intricate details of Raycasting and texture mapping.
The Basics of Raycasting
The first thing to understand is that Raycasting is not true 3D, but rather rendering a 2D world in pseudo 3D. Therefore, all movement and game positions consist of only x and y positions, with no height or z positions.
The entire game world consists of a grid, with some blocks in the grid being populated with walls and others being empty. An example of this is shown in the picture below:
In the current version of the game, the world map is implemented as a list of strings, where each character in the string represents a block in the grid. The ‘0’ character represents an empty block, and all other numbers represent a wall. The numbers ‘1’, ‘2’, and ‘3’ are used to show different wall textures according to the different numbers, something covered later in this post.
game_map = [
'11111111111111111111',
'10000000000003330001',
'10011100000000000001',
'10030000000000000001',
'10020000000000300001',
'10020001110000000001',
'10000330000000000001',
'10000330000000000001',
'10000330000000000001',
'10000330000000000001',
'10000330000000000001',
'10000330000000000001',
'10020000000000300001',
'10020001110000000001',
'10000330000000000001',
'10000330000000000001',
'10020000000000300001',
'10020001110000000001',
'10000330000000000001',
'11111111111111111111'
]
This is then converted into a dictionary as follows:
world_map = {}
for j, row in enumerate(game_map):
for i, char in enumerate(row):
if char != '0':
if char == '1':
world_map[(i * GRID_BLOCK, j * GRID_BLOCK)] = '1'
elif char == '2':
world_map[(i * GRID_BLOCK, j * GRID_BLOCK)] = '2'
elif char == '3':
world_map[(i * GRID_BLOCK, j * GRID_BLOCK)] = '3'
The player is placed on this grid with a x and y coordinates determining the player’s position on the grid. Along with the x and y coordinates, the player also has a viewing angle, i.e., a direction the player is facing.
Now that we have the foundation in place, we can get to the raycasting.
To understand this concept, imagine a line originating from the player and heading off in the direction the player is facing.
Now, this is not an endless line, but rather a line that keeps expanding from one world grid line to the next. (this is done with a for loop).
At every point where this ‘ray’ intersects a grid line on the game world, a check is done to determine if the grid line in question is a wall or not.
If it is a wall, the loop expanding the line is stopped, and the x and y coordinates where the wall was intersected will be noted. We will use this a bit later when drawing the pseudo-3D rendering of the world.
The above is the simplest form of raycasting. However, a single ray will not give us a usable amount of information to do the pseudo-3D render with. This is where a player’s FOV (field of view) and more rays come in.
The Player FOV is an angle on the game world originating at the player and extending out in a triangular form. This determines where the player’s visible range at present begins and ends. For this game, I will use a FOV of 60% (i.e., pi/3).
To change the FOV, the following can be used as a guide:
Radians | Degrees |
π / 6 | 30° |
π / 4 | 45° |
π / 3 | 60° |
π / 2 | 90° |
π | 180° |
Within this FOV, several rays will be generated, exactly as per the single one in the example discussed earlier.
In this game, a value of 480 rays has been defined, which will be generated within the FOV, so the process above for a single ray will be repeated 480 times, with each ray cast having its angle increased by a marginal amount from the previous ray.
The angle of the first ray will be determined as follows:
Starting angle = Player Angle – Half the FOV
Where Player Angle is defined as the center point of direction player is facing.
For each subsequent ray, the angle of the ray will be increased by a delta angle calculated as followed:
Delta Angle = FOV/Number of Rays
This will allow for a sufficient set of information to draw a pseudo-3D rendering from.
To see how this is implemented, please look at lines 6 to 39 in the raycasting.py file.
Sine and Cosine functions are used to determine the intersecting coordinates, and if you require a refresher on these functions, I recommend this web article from mathisfun.com.
For calculating the y coordinate where the ray intersects with a wall, the following formula is used:
y = (player y) + depth * sin(ray angle)
And to calculate the x coordinate where the ray intersects with a wall, the following formula is used:
x = (player x) + depth * cos(ray angle)
For depth value in the above formulas, a sequence of numbers would usually be looped through, starting at 0 and ending at some defined maximum depth.
The above formulas would then be executed at each new depth level to get the corresponding x and y coordinates.
This does provide the desired results, but it is not very optimized.
To improve the performance of this operation, the Digital Differential Analyzer (DDA) algorithm will be used. At a high level, the DDA algorithm functions by not checking every pixel of the 2D game world for an intersection of a ray and a wall but only checking on the grid lines of the 2D world (the only place where walls can occur).
To implement the DDA algorithm, we are going to need four extra variables in conjunction with the Player x and y coordinates, namely:
dx and dy – these two variables will determine the step size to the next grid line. Based on the direction of the angle, these either have the value of 1 or -1.
gx and gy – This will be the x and y coordinates of the grid lines that will be iterated through, starting with the grid line the closest to the player x and y position. The initial value is determined using the following function, located in the common.py file:
def align_grid(x, y):
return (x // GRID_BLOCK) * GRID_BLOCK, (y // GRID_BLOCK) * GRID_BLOCK
This will ensure that the returned x and y coordinates are located on the closet grid line (based on game world tile size). For reference, the // operator in Python is floor division and rounds the resulting number to the nearest whole number down.
To determine the depth to the next y-axis grid line, the following equation will be used:
Depth Y = (gx – player x) / cos (ray angle)
And to determine the depth of the next x-axis grid line, this equation is used:
Depth X = (gy – player y) / sin (ray angle)
The below two code blocks implement what was just described, the first block of code is to determine intersections with walls on the y axis of the world map:
# checks for walls on y axis
gx, dx = (xm + GRID_BLOCK, 1) if cos_a >= 0 else (xm, -1)
for count in range(0, MAX_DEPTH, GRID_BLOCK):
depth_y = (gx - px) / cos_a
y = py + depth_y * sin_a
tile_y = align_grid(gx + dx, y)
if tile_y in world_map:
# Ray has intersection with wall
texture_y = world_map[tile_y]
ray_col_y = True
break
gx += dx * GRID_BLOCK
And the next block of code is to determine intersections with walls on the x axis of the world map:
# checks for walls on x axis
gy, dy = (ym + GRID_BLOCK, 1) if sin_a >= 0 else (ym, -1)
for count in range(0, MAX_DEPTH, GRID_BLOCK):
depth_x = (gy - py) / sin_a
x = px + depth_x * cos_a
tile_x = align_grid(x, gy + dy)
if tile_x in world_map:
# Ray has intersection with wall
texture_x = world_map[tile_x]
ray_col_x = True
break
gy += dy * GRID_BLOCK
texture_x and texture_y are used to store the index of the texture to display on the wall. We will cover this later in this post.
Now that we have the raycasting portion covered, which is the most complex, we can focus on simply rendering the pseudo-3D graphics to the screen.
At a very high level, the basic concept of how the pseudo-3D graphics will be created, is to draw a rectangle for every ray that has intersected a wall. The x position of the rectangle will be based on the angle of the ray. The y position will be determined based on the distance of the wall from the player, with a width of the rectangle equal to the distance between the rays (calculated with Window resolution width / Number of Rays) and a user-defined height.
This will create a very basic pseudo-3D effect, and it would be much nicer using textured walls.
To implement textured walls the concept remains the same, but instead of just drawing rectangles, we will copy a small strip from a texture image and draw that to the screen instead.
In the code blocks above, there were two variables texture_x and texture_y. Where a wall intersection did occur these variables will contain a value of ‘1’, ‘2’ or ‘3’ based on the value in the world map, and these correspond to different textures that are loaded in a dictionary as follows:
textures = {
'1': pygame.image.load('images/textures/1.png').convert(),
'2': pygame.image.load('images/textures/2.png').convert(),
'3': pygame.image.load('images/textures/3.png').convert(),
'S': pygame.image.load('images/textures/sky.png').convert()
}
Firstly the correct section of the texture needs to be loaded based on the ray’s position on the wall. This is done as follows:
wall_column = textures[texture].subsurface(offset * TEXTURE_SCALE, 0, TEXTURE_SCALE, TEXTURE_HEIGHT)
Depending if it is for a x-axis or y-axis wall, the follwoing values will be as follows:
For a x-axis wall:
texture = texture_x
offset = int(x) % GRID_BLOCK
Where x is the x coordinate of the wall intersection.
And for a y-axis wall:
texture = texture_y
offset = int(y) % GRID_BLOCK
Where y is the y coordinate of the wall intersection.
Next, the section of the texture needs to be resized correctly based on its distance from the player as follows:
wall_column = pygame.transform.scale(wall_column, (SCALE, projected_height))
Where the values are determined as below:
projected_height = min(int(WALL_HEIGHT / depth), 2 * resY)
resY = Window Resolution Height
For a x-axis wall:
depth = max((depth_x * math.cos(player_angle – cur_angle)),0.00001)
For a y-axis wall:
depth = max((depth_y * math.cos(player_angle – cur_angle)),0.00001)
The last thing to do then is to draw the resized texture portion to the screen:
sc.blit(wall_column, (ray * SCALE, HALF_HEIGHT - projected_height // 2))
The above operations of copying a section of a texture, resizing it, and drawing it to the screen is done for every ray that intersects a wall.
The last thing to do and by far the least complex is to draw in the sky box and the floor. The sky box is simply an image, loaded in the texture dictionary under the ‘S’ key, which is drawn to the screen. The sky box is drawn in three blocks:
sky_offset = -5 * math.degrees(angle) % resX
self.screen.blit(self.textures['S'], (sky_offset, 0))
self.screen.blit(self.textures['S'], (sky_offset - resX, 0))
self.screen.blit(self.textures['S'], (sky_offset + resX, 0))
This ensures that no gap appears as the player turns and creates the impression of an endless sky.
Lastly, for the floor, a solid color rectangle is drawn as below:
pygame.draw.rect(self.screen, GREY, (0, HALF_HEIGHT, resX, HALF_HEIGHT))
For reference, the following PyGame functions are used in the game up to this point:
pygame.init
Used to initialize pygame modules and get them ready to use.
pygame.display.set_mode
Used to initialize a window to display the game.
pygame.image.load
Used to load an image file from the supplied path into a variable to be used when needed.
pygame.Surface.subsurface
Used to get a copy of a section of an image (surface) based on the supplied x position,y position, width, and height values.
pygame.transform.scale
Used to resize an image (surface) to the supplied width and height.
pygame.Surface.blit
Used to draw images to the screen.
pygame.display.flip
Used to update the full display Surface to the screen.
pygame.Surface.fill
Used to fill the display surface with a background color.
pygame.draw.rect
Used to draw a rectangle to the screen (used for the floor).
Also used pygame.key.get_pressed, pygame.event.get and pygame.mouse methods for user input.
Collision Detection
Because the game plays out in a 2D world, collision detection is rather straightforward.
The player has a square hitbox, and every time the player inputs a movement, the check_collision function is called with the new x and y positions the player wants to move to. The function then uses the new x and y positions to determine the player hitbox and check if it is in contact with any walls; if so, the move is not allowed. Otherwise, the player x and y positions are updated to the new positions.
Here is the check_collision function that forms part of the Player class:
def check_collision(self, new_x, new_y):
player_location = mapping(new_x , new_y)
if player_location in world_map:
# collision
print("Center Collision" + str(new_x) + " " + str(new_y))
return
player_location = mapping(new_x - HALF_PLAYER_MARGIN, new_y - HALF_PLAYER_MARGIN)
if player_location in world_map:
# collision
print("Top Left Corner Collision" + str(new_x) + " " + str(new_y))
return
player_location = mapping(new_x + HALF_PLAYER_MARGIN, new_y - HALF_PLAYER_MARGIN)
if player_location in world_map:
# collision
print("Top Right Corner Collision" + str(new_x) + " " + str(new_y))
return
player_location = mapping(new_x - HALF_PLAYER_MARGIN, new_y + HALF_PLAYER_MARGIN)
if player_location in world_map:
# collision
print("Bottom Left Corner Collision" + str(new_x) + " " + str(new_y))
return
player_location = mapping(new_x + HALF_PLAYER_MARGIN, new_y + HALF_PLAYER_MARGIN)
if player_location in world_map:
# collision
print("Bottom Right Corner Collision" + str(new_x) + " " + str(new_y))
return
self.x = new_x
self.y = new_y
Here is a video of the current version of the game in action:
The current version of this game is still a work in progress, but if you are interested, the source code can be downloaded here and the executable here.
Some of the next things on the to-do list are loading levels from the file, adding sprites to the game world, and adding some interactive world items, such as doors that open and close.
I will keep creating posts on this topic as I progress with this project.
Прежде чем мы начнём программировать что-то полезное на Python, давайте закодим что-нибудь интересное. Например, свою игру, где нужно не дать шарику упасть, типа Арканоида. Вы, скорее всего, играли в детстве во что-то подобное, поэтому освоиться будет просто.
Логика игры
Есть игровое поле — простой прямоугольник с твёрдыми границами. Когда шарик касается стенки или потолка, он отскакивает в другую сторону. Если он упадёт на пол — вы проиграли. Чтобы этого не случилось, внизу вдоль пола летает платформа, а вы ей управляете с помощью стрелок. Ваша задача — подставлять платформу под шарик как можно дольше. За каждое удачное спасение шарика вы получаете одно очко.
Алгоритм
Чтобы реализовать такую логику игры, нужно предусмотреть такие сценарии поведения:
- игра начинается;
- шарик начинает двигаться;
- если нажаты стрелки влево или вправо — двигаем платформу;
- если шарик коснулся стенок, потолка или платформы — делаем отскок;
- если шарик коснулся платформы — увеличиваем счёт на единицу;
- если шарик упал на пол — выводим сообщение и заканчиваем игру.
Хитрость в том, что всё это происходит параллельно и независимо друг от друга. То есть пока шарик летает, мы вполне можем двигать платформу, а можем и оставить её на месте. И когда шарик отскакивает от стен, это тоже не мешает другим объектам двигаться и взаимодействовать между собой.
Получается, что нам нужно определить три класса — платформу, сам шарик и счёт, и определить, как они реагируют на действия друг друга. Поле нам самим определять не нужно — для этого есть уже готовая библиотека. А потом в этих классах мы пропишем методы — они как раз и будут отвечать за поведение наших объектов.
Весь кайф в том, что мы всё это задаём один раз, а потом объекты сами разбираются, как им реагировать друг на друга и что делать в разных ситуациях. Мы не прописываем жёстко весь алгоритм, а задаём правила игры — а для этого классы подходят просто идеально.
По коням, пишем на Python
Для этого проекта вам потребуется установить и запустить среду Python. Как это сделать — читайте в нашей статье.
Начало программы
Чтобы у нас появилась графика в игре, используем библиотеку Tkinter. Она входит в набор стандартных библиотек Python и позволяет рисовать простейшие объекты — линии, прямоугольники, круги и красить их в разные цвета. Такой простой Paint, только для Python.
Чтобы создать окно, где будет видна графика, используют класс Tk(). Он просто делает окно, но без содержимого. Чтобы появилось содержимое, создают холст — видимую часть окна. Именно на нём мы будем рисовать нашу игру. За холст отвечает класс Canvas(), поэтому нам нужно будет создать свой объект из этого класса и дальше уже работать с этим объектом.
Если мы принудительно не ограничим скорость платформы, то она будет перемещаться мгновенно, ведь компьютер считает очень быстро и моментально передвинет её к другому краю. Поэтому мы будем искусственно ограничивать время движения, а для этого нам понадобится модуль Time — он тоже стандартный.
Последнее, что нам глобально нужно, — задавать случайным образом начальное положение шарика и платформы, чтобы было интереснее играть. За это отвечает модуль Random — он помогает генерировать случайные числа и перемешивать данные.
Запишем всё это в виде кода на Python:
# подключаем графическую библиотеку
from tkinter import *
# подключаем модули, которые отвечают за время и случайные числа
import time
import random
# создаём новый объект — окно с игровым полем. В нашем случае переменная окна называется tk, и мы его сделали из класса Tk() — он есть в графической библиотеке
tk = Tk()
# делаем заголовок окна — Games с помощью свойства объекта title
tk.title('Game')
# запрещаем менять размеры окна, для этого используем свойство resizable
tk.resizable(0, 0)
# помещаем наше игровое окно выше остальных окон на компьютере, чтобы другие окна не могли его заслонить
tk.wm_attributes('-topmost', 1)
# создаём новый холст — 400 на 500 пикселей, где и будем рисовать игру
canvas = Canvas(tk, width=500, height=400, highlightthickness=0)
# говорим холсту, что у каждого видимого элемента будут свои отдельные координаты
canvas.pack()
# обновляем окно с холстом
tk.update()
Мы подключили все нужные библиотеки, сделали и настроили игровое поле. Теперь займёмся классами.
Шарик
Сначала проговорим словами, что нам нужно от шарика. Он должен уметь:
- задавать своё начальное положение и направление движение;
- понимать, когда он коснулся платформы;
- рисовать сам себя и понимать, когда нужно отрисовать себя в новом положении (например, после отскока от стены).
Этого достаточно, чтобы шарик жил своей жизнью и умел взаимодействовать с окружающей средой. При этом нужно не забыть о том, что каждый класс должен содержать конструктор — код, который отвечает за создание нового объекта. Без этого сделать шарик не получится. Запишем это на Python:
# Описываем класс Ball, который будет отвечать за шарик
class Ball:
# конструктор — он вызывается в момент создания нового объекта на основе этого класса
def __init__(self, canvas, paddle, score, color):
# задаём параметры объекта, которые нам передают в скобках в момент создания
self.canvas = canvas
self.paddle = paddle
self.score = score
# цвет нужен был для того, чтобы мы им закрасили весь шарик
# здесь появляется новое свойство id, в котором хранится внутреннее название шарика
# а ещё командой create_oval мы создаём круг радиусом 15 пикселей и закрашиваем нужным цветом
self.id = canvas.create_oval(10,10, 25, 25, fill=color)
# помещаем шарик в точку с координатами 245,100
self.canvas.move(self.id, 245, 100)
# задаём список возможных направлений для старта
starts = [-2, -1, 1, 2]
# перемешиваем его
random.shuffle(starts)
# выбираем первый из перемешанного — это будет вектор движения шарика
self.x = starts[0]
# в самом начале он всегда падает вниз, поэтому уменьшаем значение по оси y
self.y = -2
# шарик узнаёт свою высоту и ширину
self.canvas_height = self.canvas.winfo_height()
self.canvas_width = self.canvas.winfo_width()
# свойство, которое отвечает за то, достиг шарик дна или нет. Пока не достиг, значение будет False
self.hit_bottom = False
# обрабатываем касание платформы, для этого получаем 4 координаты шарика в переменной pos (левая верхняя и правая нижняя точки)
def hit_paddle(self, pos):
# получаем кординаты платформы через объект paddle (платформа)
paddle_pos = self.canvas.coords(self.paddle.id)
# если координаты касания совпадают с координатами платформы
if pos[2] >= paddle_pos[0] and pos[0] <= paddle_pos[2]:
if pos[3] >= paddle_pos[1] and pos[3] <= paddle_pos[3]:
# увеличиваем счёт (обработчик этого события будет описан ниже)
self.score.hit()
# возвращаем метку о том, что мы успешно коснулись
return True
# возвращаем False — касания не было
return False
# обрабатываем отрисовку шарика
def draw(self):
# передвигаем шарик на заданные координаты x и y
self.canvas.move(self.id, self.x, self.y)
# запоминаем новые координаты шарика
pos = self.canvas.coords(self.id)
# если шарик падает сверху
if pos[1] <= 0:
# задаём падение на следующем шаге = 2
self.y = 2
# если шарик правым нижним углом коснулся дна
if pos[3] >= self.canvas_height:
# помечаем это в отдельной переменной
self.hit_bottom = True
# выводим сообщение и количество очков
canvas.create_text(250, 120, text='Вы проиграли', font=('Courier', 30), fill='red')
# если было касание платформы
if self.hit_paddle(pos) == True:
# отправляем шарик наверх
self.y = -2
# если коснулись левой стенки
if pos[0] <= 0:
# движемся вправо
self.x = 2
# если коснулись правой стенки
if pos[2] >= self.canvas_width:
# движемся влево
self.x = -2
Платформа
Сделаем то же самое для платформы — сначала опишем её поведение словами, а потом переведём в код. Итак, вот что должна уметь платформа:
- двигаться влево или вправо в зависимости от нажатой стрелки;
- понимать, когда игра началась и можно двигаться.
А вот как это будет в виде кода:
# Описываем класс Paddle, который отвечает за платформы
class Paddle:
# конструктор
def __init__(self, canvas, color):
# canvas означает, что платформа будет нарисована на нашем изначальном холсте
self.canvas = canvas
# создаём прямоугольную платформу 10 на 100 пикселей, закрашиваем выбранным цветом и получаем её внутреннее имя
self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)
# задаём список возможных стартовых положений платформы
start_1 = [40, 60, 90, 120, 150, 180, 200]
# перемешиваем их
random.shuffle(start_1)
# выбираем первое из перемешанных
self.starting_point_x = start_1[0]
# перемещаем платформу в стартовое положение
self.canvas.move(self.id, self.starting_point_x, 300)
# пока платформа никуда не движется, поэтому изменений по оси х нет
self.x = 0
# платформа узнаёт свою ширину
self.canvas_width = self.canvas.winfo_width()
# задаём обработчик нажатий
# если нажата стрелка вправо — выполняется метод turn_right()
self.canvas.bind_all('<KeyPress-Right>', self.turn_right)
# если стрелка влево — turn_left()
self.canvas.bind_all('<KeyPress-Left>', self.turn_left)
# пока игра не началась, поэтому ждём
self.started = False
# как только игрок нажмёт Enter — всё стартует
self.canvas.bind_all('<KeyPress-Return>', self.start_game)
# движемся вправо
def turn_right(self, event):
# будем смещаться правее на 2 пикселя по оси х
self.x = 2
# движемся влево
def turn_left(self, event):
# будем смещаться левее на 2 пикселя по оси х
self.x = -2
# игра начинается
def start_game(self, event):
# меняем значение переменной, которая отвечает за старт
self.started = True
# метод, который отвечает за движение платформы
def draw(self):
# сдвигаем нашу платформу на заданное количество пикселей
self.canvas.move(self.id, self.x, 0)
# получаем координаты холста
pos = self.canvas.coords(self.id)
# если мы упёрлись в левую границу
if pos[0] <= 0:
# останавливаемся
self.x = 0
# если упёрлись в правую границу
elif pos[2] >= self.canvas_width:
# останавливаемся
self.x = 0
Счёт
Можно было не выделять счёт в отдельный класс и каждый раз обрабатывать вручную. Но здесь реально проще сделать класс, задать нужные методы, чтобы они сами потом разобрались, что и когда делать.
От счёта нам нужно только одно (кроме конструктора) — чтобы он правильно реагировал на касание платформы, увеличивал число очков и выводил их на экран:
# Описываем класс Score, который отвечает за отображение счетов
class Score:
# конструктор
def __init__(self, canvas, color):
# в самом начале счёт равен нулю
self.score = 0
# будем использовать наш холст
self.canvas = canvas
# создаём надпись, которая показывает текущий счёт, делаем его нужно цвета и запоминаем внутреннее имя этой надписи
self.id = canvas.create_text(450, 10, text=self.score, font=('Courier', 15), fill=color)
# обрабатываем касание платформы
def hit(self):
# увеличиваем счёт на единицу
self.score += 1
# пишем новое значение счёта
self.canvas.itemconfig(self.id, text=self.score)
Игра
У нас всё готово для того, чтобы написать саму игру. Мы уже провели необходимую подготовку всех элементов, и нам остаётся только создать конкретные объекты шарика, платформы и счёта и сказать им, в каком порядке мы будем что делать.
Смысл игры в том, чтобы не уронить шарик. Пока этого не произошло — всё движется, но как только шарик упал — нужно показать сообщение о конце игры и остановить программу.
Посмотрите, как лаконично выглядит код непосредственно самой игры:
# создаём объект — зелёный счёт
score = Score(canvas, 'green')
# создаём объект — белую платформу
paddle = Paddle(canvas, 'White')
# создаём объект — красный шарик
ball = Ball(canvas, paddle, score, 'red')
# пока шарик не коснулся дна
while not ball.hit_bottom:
# если игра началась и платформа может двигаться
if paddle.started == True:
# двигаем шарик
ball.draw()
# двигаем платформу
paddle.draw()
# обновляем наше игровое поле, чтобы всё, что нужно, закончило рисоваться
tk.update_idletasks()
# обновляем игровое поле, и смотрим за тем, чтобы всё, что должно было быть сделано — было сделано
tk.update()
# замираем на одну сотую секунды, чтобы движение элементов выглядело плавно
time.sleep(0.01)
# если программа дошла досюда, значит, шарик коснулся дна. Ждём 3 секунды, пока игрок прочитает финальную надпись, и завершаем игру
time.sleep(3)
# подключаем графическую библиотеку
from tkinter import *
# подключаем модули, которые отвечают за время и случайные числа
import time
import random
# создаём новый объект — окно с игровым полем. В нашем случае переменная окна называется tk, и мы его сделали из класса Tk() — он есть в графической библиотеке
tk = Tk()
# делаем заголовок окна — Games с помощью свойства объекта title
tk.title('Game')
# запрещаем менять размеры окна, для этого используем свойство resizable
tk.resizable(0, 0)
# помещаем наше игровое окно выше остальных окон на компьютере, чтобы другие окна не могли его заслонить. Попробуйте 🙂
tk.wm_attributes('-topmost', 1)
# создаём новый холст — 400 на 500 пикселей, где и будем рисовать игру
canvas = Canvas(tk, width=500, height=400, highlightthickness=0)
# говорим холсту, что у каждого видимого элемента будут свои отдельные координаты
canvas.pack()
# обновляем окно с холстом
tk.update()
# Описываем класс Ball, который будет отвечать за шарик
class Ball:
# конструктор — он вызывается в момент создания нового объекта на основе этого класса
def __init__(self, canvas, paddle, score, color):
# задаём параметры объекта, которые нам передают в скобках в момент создания
self.canvas = canvas
self.paddle = paddle
self.score = score
# цвет нужен был для того, чтобы мы им закрасили весь шарик
# здесь появляется новое свойство id, в котором хранится внутреннее название шарика
# а ещё командой create_oval мы создаём круг радиусом 15 пикселей и закрашиваем нужным цветом
self.id = canvas.create_oval(10,10, 25, 25, fill=color)
# помещаем шарик в точку с координатами 245,100
self.canvas.move(self.id, 245, 100)
# задаём список возможных направлений для старта
starts = [-2, -1, 1, 2]
# перемешиваем его
random.shuffle(starts)
# выбираем первый из перемешанного — это будет вектор движения шарика
self.x = starts[0]
# в самом начале он всегда падает вниз, поэтому уменьшаем значение по оси y
self.y = -2
# шарик узнаёт свою высоту и ширину
self.canvas_height = self.canvas.winfo_height()
self.canvas_width = self.canvas.winfo_width()
# свойство, которое отвечает за то, достиг шарик дна или нет. Пока не достиг, значение будет False
self.hit_bottom = False
# обрабатываем касание платформы, для этого получаем 4 координаты шарика в переменной pos (левая верхняя и правая нижняя точки)
def hit_paddle(self, pos):
# получаем кординаты платформы через объект paddle (платформа)
paddle_pos = self.canvas.coords(self.paddle.id)
# если координаты касания совпадают с координатами платформы
if pos[2] >= paddle_pos[0] and pos[0] <= paddle_pos[2]:
if pos[3] >= paddle_pos[1] and pos[3] <= paddle_pos[3]:
# увеличиваем счёт (обработчик этого события будет описан ниже)
self.score.hit()
# возвращаем метку о том, что мы успешно коснулись
return True
# возвращаем False — касания не было
return False
# метод, который отвечает за движение шарика
def draw(self):
# передвигаем шарик на заданный вектор x и y
self.canvas.move(self.id, self.x, self.y)
# запоминаем новые координаты шарика
pos = self.canvas.coords(self.id)
# если шарик падает сверху
if pos[1] <= 0:
# задаём падение на следующем шаге = 2
self.y = 2
# если шарик правым нижним углом коснулся дна
if pos[3] >= self.canvas_height:
# помечаем это в отдельной переменной
self.hit_bottom = True
# выводим сообщение и количество очков
canvas.create_text(250, 120, text='Вы проиграли', font=('Courier', 30), fill='red')
# если было касание платформы
if self.hit_paddle(pos) == True:
# отправляем шарик наверх
self.y = -2
# если коснулись левой стенки
if pos[0] <= 0:
# движемся вправо
self.x = 2
# если коснулись правой стенки
if pos[2] >= self.canvas_width:
# движемся влево
self.x = -2
# Описываем класс Paddle, который отвечает за платформы
class Paddle:
# конструктор
def __init__(self, canvas, color):
# canvas означает, что платформа будет нарисована на нашем изначальном холсте
self.canvas = canvas
# создаём прямоугольную платформу 10 на 100 пикселей, закрашиваем выбранным цветом и получаем её внутреннее имя
self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)
# задаём список возможных стартовых положений платформы
start_1 = [40, 60, 90, 120, 150, 180, 200]
# перемешиваем их
random.shuffle(start_1)
# выбираем первое из перемешанных
self.starting_point_x = start_1[0]
# перемещаем платформу в стартовое положение
self.canvas.move(self.id, self.starting_point_x, 300)
# пока платформа никуда не движется, поэтому изменений по оси х нет
self.x = 0
# платформа узнаёт свою ширину
self.canvas_width = self.canvas.winfo_width()
# задаём обработчик нажатий
# если нажата стрелка вправо — выполняется метод turn_right()
self.canvas.bind_all('<KeyPress-Right>', self.turn_right)
# если стрелка влево — turn_left()
self.canvas.bind_all('<KeyPress-Left>', self.turn_left)
# пока платформа не двигается, поэтому ждём
self.started = False
# как только игрок нажмёт Enter — всё стартует
self.canvas.bind_all('<KeyPress-Return>', self.start_game)
# движемся вправо
def turn_right(self, event):
# будем смещаться правее на 2 пикселя по оси х
self.x = 2
# движемся влево
def turn_left(self, event):
# будем смещаться левее на 2 пикселя по оси х
self.x = -2
# игра начинается
def start_game(self, event):
# меняем значение переменной, которая отвечает за старт движения платформы
self.started = True
# метод, который отвечает за движение платформы
def draw(self):
# сдвигаем нашу платформу на заданное количество пикселей
self.canvas.move(self.id, self.x, 0)
# получаем координаты холста
pos = self.canvas.coords(self.id)
# если мы упёрлись в левую границу
if pos[0] <= 0:
# останавливаемся
self.x = 0
# если упёрлись в правую границу
elif pos[2] >= self.canvas_width:
# останавливаемся
self.x = 0
# Описываем класс Score, который отвечает за отображение счетов
class Score:
# конструктор
def __init__(self, canvas, color):
# в самом начале счёт равен нулю
self.score = 0
# будем использовать наш холст
self.canvas = canvas
# создаём надпись, которая показывает текущий счёт, делаем его нужно цвета и запоминаем внутреннее имя этой надписи
self.id = canvas.create_text(450, 10, text=self.score, font=('Courier', 15), fill=color)
# обрабатываем касание платформы
def hit(self):
# увеличиваем счёт на единицу
self.score += 1
# пишем новое значение счёта
self.canvas.itemconfig(self.id, text=self.score)
# создаём объект — зелёный счёт
score = Score(canvas, 'green')
# создаём объект — белую платформу
paddle = Paddle(canvas, 'White')
# создаём объект — красный шарик
ball = Ball(canvas, paddle, score, 'red')
# пока шарик не коснулся дна
while not ball.hit_bottom:
# если игра началась и платформа может двигаться
if paddle.started == True:
# двигаем шарик
ball.draw()
# двигаем платформу
paddle.draw()
# обновляем наше игровое поле, чтобы всё, что нужно, закончило рисоваться
tk.update_idletasks()
# обновляем игровое поле и смотрим за тем, чтобы всё, что должно было быть сделано — было сделано
tk.update()
# замираем на одну сотую секунды, чтобы движение элементов выглядело плавно
time.sleep(0.01)
# если программа дошла досюда, значит, шарик коснулся дна. Ждём 3 секунды, пока игрок прочитает финальную надпись, и завершаем игру
time.sleep(3)
Что дальше
На основе этого кода вы можете сделать свою модификацию игры:
- добавить второй шарик;
- раскрасить элементы в другой цвет;
- поменять размеры шарика; поменять скорость платформы;
- сделать всё это сразу;
- поменять логику программы на свою.