Fork/Clone Ch.12 User Control Jedi Training
Fork/Clone Ch.12 User Control Jedi Training
To make a cool game we need to interact with the user. How do we interact with the user for example letting the user move an object on the screen? We can do this with the mouse, with the keyboard, or with a game controller. This chapter will attempt to teach you how to take control of these things by coding their usage.
The key to managing mouse motion is to override the on_mouse_motion method in the arcade.Window class. That method is called every time the mouse moves. The method definition looks like this:
def on_mouse_motion(self, x, y, dx, dy):
The x and y are the coordinates of the mouse pointer and the dx and dy represent the change in x and y since the last time the method was called.
Often when controlling a graphical item on the screen with the mouse, we do not want to see the mouse pointer. If you don’t want to see the mouse pointer, in the __init__ method, call the following method in the parent class:
self.set_mouse_visible(False)
The example below takes our Ball class we made in the last chapter and moves it around the screen with the mouse.
import arcade
SW = 640
SH = 480
class Ball:
def __init__(self, pos_x, pos_y, rad, col):
self.pos_x = pos_x
self.pos_y = pos_y
self.rad = rad
self.col = col
def draw(self):
arcade.draw_circle_filled(self.pos_x, self.pos_y, self.rad, self.col)
class MyGame(arcade.Window):
def __init__(self, width, height, title):
super().__init__(width, height, title)
self.set_mouse_visible(False)
arcade.set_background_color(arcade.color.ASH_GREY)
self.ball = Ball(50, 50, 15, arcade.color.AUBURN)
def on_draw(self):
arcade.start_render()
self.ball.draw()
def on_mouse_motion(self, x, y, dx, dy):
self.ball.pos_x = x
self.ball.pos_y = y
def main():
window = MyGame(SW, SH, "Mouse Example")
arcade.run()
if __name__=="__main__":
main()
You can also process mouse clicks by defining an on_mouse_press method which is called when a user presses a mouse button. This code can come in handy when making graphics on a screen and you quickly want to find the coordinates of where you want to draw something. Try changing the set_mouse_visible to True and deleting the code where the ball follows the mouse. Now you can click in the screen and see the coordinates printed in the python Run window.
def on_mouse_press(self, x, y, button, modifiers):
if button == arcade.MOUSE_BUTTON_LEFT:
print("Left mouse button pressed at", x, y)
elif button == arcade.MOUSE_BUTTON_RIGHT:
print("Right mouse button pressed at", x, y)
Moving with the keyboard is similar to our bouncing ball example. There are just two differences:
We control the change_x and change_y with the keyboard
When we hit the edge of the screen we stop, rather than bounce.
To detect when a key is hit, we override the on_key_press method. We might think of hitting a key as one event. But it is actually two. When the key is pressed, we start moving. When the key is released we stop moving. That makes for two events. Releasing a key is controlled by on_key_release.
These methods have a key variable as a parameter that can be compared with an if statement to the values in the:
def on_key_press(self, key, modifiers):
if key == arcade.key.LEFT:
print("Left key hit")
elif key == arcade.key.A:
print("The 'a' key was hit")
We can use the program below to move a ball around the screen. Notice we made a speed variable at the top of the program. If a key is pressed (on_key_press) we set the appropriate dx or dy to either a positive SPEED or a negative SPEED. If a key is released (on_key_release) we set the appropriate dx and dy to 0 so it will stop moving.
If you try this program you will see that you can go right off the window. This could be a problem. Can you think of a way to code this program so for example if you continue hitting the Right Arrow key that the ball won't go beyond the right side of the window?
import arcade
SW = 640
SH = 480
SPEED = 3 # 3 x 60 = 180 pixels/second
class Ball:
def __init__(self, pos_x, pos_y, dx, dy, rad, col):
self.pos_x = pos_x
self.pos_y = pos_y
self.dx = dx
self.dy = dy
self.rad = rad
self.col = col
def draw(self):
arcade.draw_circle_filled(self.pos_x, self.pos_y, self.rad, self.col)
def update(self):
self.pos_y += self.dy
self.pos_x += self.dx
class MyGame(arcade.Window):
def __init__(self, width, height, title):
super().__init__(width, height, title)
self.set_mouse_visible(False)
arcade.set_background_color(arcade.color.ASH_GREY)
self.ball = Ball(50, 50, 0, 0, 15, arcade.color.AUBURN)
def on_draw(self):
arcade.start_render()
self.ball.draw()
def on_update(self, dt):
self.ball.update()
def on_key_press(self, key, modifiers):
if key == arcade.key.LEFT:
self.ball.dx = -SPEED
elif key == arcade.key.RIGHT:
self.ball.dx = SPEED
elif key == arcade.key.UP:
self.ball.dy = SPEED
elif key == arcade.key.DOWN:
self.ball.dy = -SPEED
def on_key_release(self, key, modifiers):
if key == arcade.key.LEFT or key == arcade.key.RIGHT:
self.ball.dx = 0
elif key == arcade.key.UP or key == arcade.key.DOWN:
self.ball.dy = 0
def main():
window = MyGame(SW, SH, "Key Press Example")
arcade.run()
if __name__=="__main__":
main()
Working with game controllers is a bit more complex. We have several 8BitDo Pro2 Game Controllers which will work with our Macs either wired or through bluetooth.
Turn the mode switch to X
Press start to turn on the controller, the 1st LED starts to blink
Hold the pair button (small round button on back) for 3 seconds to enter its pairing mode. LED starts to rotate from left to right. (this is required for the very first time only)
Go to your Apple device's Bluetooth setting and turn it on, pair with [8BitDo Pro 2]. LED becomes solid when connection is successful
Turn the mode switch to X
Connect the controller to the USB port of your Apple device, wait till the controller is successfully recognized by your Apple device to play
We can get a list of all game pads that are plugged in with the get_joysticks function. This will either return a list, or it will return nothing at all if there are no game pads.
Below is a block of code that can be put in an __init__ method of your MyGame Class that will create an instance variable to represent a game pad if one exists.
joysticks = arcade.get_joysticks()
if joysticks:
self.joystick = joysticks[0]
self.joystick.open()
self.joystick.push_handlers(self)
else:
print("There are no joysticks.")
self.joystick = None
There are actually 3 axes for the left and right joysticks AND triggers. self.joystick.x and self.joystick.y control up and down for the left joystick. self.joystick.z controls the left trigger. For the right joystick and trigger they are self.joystick.rx, self.joystick.ry and self.joystick.rz. You can print the position of the game controller joysticks in the on_update method with the following:
if self.joystick:
print(self.joystick.x, self.joystick.y, self.joystick.z)
Joystick Coordinates
Run the program and see the values it prints out for your game controller as you move the joystick on it around.
The values will be between -1 and +1, with 0 being a centered joystick.
The x-axis numbers will be negative if the stick goes left, positive for right.
The y-axis numbers will be opposite of what you might expect. Negative for up, positive for down.
The z-axis numbers for the trigger start on -1 when released, 0 is half-way pulled in and +1 is fully pulled in.
We can move the ball by adding the following code to the on_update:
def on_update(self):
self.ball.dx = self.joystick.x
self.ball.dy = -self.joystick.y
Notice the - we put in front of setting the y vector. If we don’t do this, the ball will move opposite of what we expect when going up/down. This is because the joystick has y values mapped opposite of how we’d normally expect.
But with this code our ball moves so slow. How do we speed it up? We can make it run 10 times faster by multiplying by 10 if we want.
def on_update(self):
self.ball.dx = self.joystick.x * 10
self.ball.dy = -self.joystick.y * 10
Or better yet, define a constant variable at the top of your program called SPEED = 10 and use that.
To can also use the buttons on the Game Controllers. We do this very much like on_key_press. Instead we use on_joybutton_press and on_joybutton_release. Here is code to determine the joybutton numbers. (button_no):
def on_joybutton_press(self, joystick, button_no):
print(f"Button {button_no} pressed!")
if button_no == 14:
#DO STUFF HERE
def on_joybutton_release(self, joystick, button_no):
print(f"Button {button_no} was released!")
What if your ball ‘drifts’ when you have the joystick centered? Joysticks are mechanical. A centered joystick might have a value not at 0, but at 0.0001 or some small number. This will make for a small “drift”. I created a variable at the top called DEAD_ZONE = 0.05
We can stop drifting by using this:
self.ball.dx = self.joystick.x * SPEED
if abs(self.ball.dx) < DEAD_ZONE:
self.ball.dx = 0
self.ball.dy = -self.joystick.y * SPEED
if abs(self.ball.dy) < DEAD_ZONE:
self.ball.dy = 0
Adding sound to your game isn’t too hard. There are two steps:
Load the sound
Play the sound
For these examples, I’m using the sound file called laser.mp3 that you downloaded in this chapter's repository. Make sure you save the file in the same directory as any Python program that tries to use it or in a Sounds folder.
The code below creates a new variable called laser_sound. It calls arcades load_sound function. It passes the filename of our sound.
self.laser_sound = arcade.load_sound("laser.mp3")
For this to work, you need to have a sound downloaded and named laser.mp3 in the same directory as your Python file. I suggest making another folder called Sounds that will store all sound effects in a game. If you do this then your code would need to look like this:
self.laser_sound = arcade.load_sound("Sounds/laser.mp3")
Load the sound in an __init__ method of any classes where you are going to play the sound.
The code to play sounds is straight forward. Just call the play_sound function, and pass in the variable that we set equal to the sound we loaded:
arcade.play_sound(self.laser_sound)
You should place this play_sound method in some kind of trigger like when the ball hits an edge or when the spacebar is pressed.
Great places to find free sounds to use in your program:
www.freesound.org is ok, but requires a login, which is a pain.
Kenney.nl has a few free sound packs you can download, and several that are cheap.
There are several types of sound file formats that you can find sounds in:
.wav - This is a raw “wave” format. The sound is not compressed at all. You do not need a special library to decompress the sound, but the sound file itself can be rather large.
.mp3 - MPEG Layer III Audio (mp3) is one of the most popular compressed sound file formats. This file format is what enabled on-line music to become popular. However some of the compression algorithms have patents on them, making it not as suitable for free software.
.m4a - Apple’s file format for compressed, but unprotected audio.
.ogg - A open-source sound file that uses Ogg-Vorbis for compression. A less popular but patent-free method of sound storage.
Arcade should be able to play files in either the mp3 or ogg format. If you have issues getting it to work, try converting the sound to a raw wav format. If you need to convert file formats, or trim a sound file, use the program Audacity.