19. User Control

How do we interact with the user? Get the user to move an object on the screen?

We can do this with the mouse, with the keyboard, or with the game controller.

19.1. Move with the Mouse

The key to managing mouse motion to override the on_mouse_motion 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. 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, and moves it around the screen with the mouse.

move_with_mouse.py
 1import arcade
 2
 3SCREEN_WIDTH = 640
 4SCREEN_HEIGHT = 480
 5
 6
 7class Ball:
 8    def __init__(self, position_x, position_y, radius, color):
 9
10        # Take the parameters of the init function above, and create instance variables out of them.
11        self.position_x = position_x
12        self.position_y = position_y
13        self.radius = radius
14        self.color = color
15
16    def draw(self):
17        """ Draw the balls with the instance variables we have. """
18        arcade.draw_circle_filled(self.position_x, self.position_y, self.radius, self.color)
19
20
21class MyGame(arcade.Window):
22
23    def __init__(self, width, height, title):
24
25        # Call the parent class's init function
26        super().__init__(width, height, title)
27
28        # Make the mouse disappear when it is over the window.
29        # So we just see our object, not the pointer.
30        self.set_mouse_visible(False)
31
32        arcade.set_background_color(arcade.color.ASH_GREY)
33
34        # Create our ball
35        self.ball = Ball(50, 50, 15, arcade.color.AUBURN)
36
37    def on_draw(self):
38        """ Called whenever we need to draw the window. """
39        arcade.start_render()
40        self.ball.draw()
41
42    def on_mouse_motion(self, x, y, dx, dy):
43        """ Called to update our objects. Happens approximately 60 times per second."""
44        self.ball.position_x = x
45        self.ball.position_y = y
46
47
48def main():
49    window = MyGame(640, 480, "Drawing Example")
50    arcade.run()
51
52
53main()

19.2. Mouse Clicks

You can also process mouse clicks by defining an on_mouse_press method:

def on_mouse_press(self, x, y, button, modifiers):
    """ Called when the user presses a mouse button. """

    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)

19.3. Move with the Keyboard

Moving with the game controller 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 arcade.key library.

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 this in a program to move a ball around the screen. See the highlighted lines in the program below:

move_with_keyboard_simple.py
 1import arcade
 2
 3SCREEN_WIDTH = 640
 4SCREEN_HEIGHT = 480
 5MOVEMENT_SPEED = 3
 6
 7
 8class Ball:
 9    def __init__(self, position_x, position_y, change_x, change_y, radius, color):
10
11        # Take the parameters of the init function above, and create instance variables out of them.
12        self.position_x = position_x
13        self.position_y = position_y
14        self.change_x = change_x
15        self.change_y = change_y
16        self.radius = radius
17        self.color = color
18
19    def draw(self):
20        """ Draw the balls with the instance variables we have. """
21        arcade.draw_circle_filled(self.position_x, self.position_y, self.radius, self.color)
22
23    def update(self):
24        # Move the ball
25        self.position_y += self.change_y
26        self.position_x += self.change_x
27
28
29class MyGame(arcade.Window):
30
31    def __init__(self, width, height, title):
32
33        # Call the parent class's init function
34        super().__init__(width, height, title)
35
36        # Make the mouse disappear when it is over the window.
37        # So we just see our object, not the pointer.
38        self.set_mouse_visible(False)
39
40        arcade.set_background_color(arcade.color.ASH_GREY)
41
42        # Create our ball
43        self.ball = Ball(50, 50, 0, 0, 15, arcade.color.AUBURN)
44
45    def on_draw(self):
46        """ Called whenever we need to draw the window. """
47        arcade.start_render()
48        self.ball.draw()
49
50    def update(self, delta_time):
51        self.ball.update()
52
53    def on_key_press(self, key, modifiers):
54        """ Called whenever the user presses a key. """
55        if key == arcade.key.LEFT:
56            self.ball.change_x = -MOVEMENT_SPEED
57        elif key == arcade.key.RIGHT:
58            self.ball.change_x = MOVEMENT_SPEED
59        elif key == arcade.key.UP:
60            self.ball.change_y = MOVEMENT_SPEED
61        elif key == arcade.key.DOWN:
62            self.ball.change_y = -MOVEMENT_SPEED
63
64    def on_key_release(self, key, modifiers):
65        """ Called whenever a user releases a key. """
66        if key == arcade.key.LEFT or key == arcade.key.RIGHT:
67            self.ball.change_x = 0
68        elif key == arcade.key.UP or key == arcade.key.DOWN:
69            self.ball.change_y = 0
70
71
72def main():
73    window = MyGame(640, 480, "Drawing Example")
74    arcade.run()
75
76
77main()

19.3.1. Keep From Moving Off Screen

Unfortunately in the prior program, there is nothing that keeps the player from moving off-screen. If we want to stop the player from moving off-screen we need some additional code.

We detect the edge by comparing position_x with the left and right side of the screen For example:

if self.position_x < 0:

But this isn’t perfect. Because the position specifies the center of the ball, by the time the x coordinate is 0 we are already have off the screen. It is better to compare it to the ball’s radius:

if self.position_x < self.radius:

What do we do once it hits the edge? Just set the value back to the edge:

# See if the ball hit the edge of the screen. If so, change direction
if self.position_x < self.radius:
    self.position_x = self.radius

Here’s a full example:

move_with_keyboard_edge_detect.py
 1import arcade
 2
 3SCREEN_WIDTH = 640
 4SCREEN_HEIGHT = 480
 5MOVEMENT_SPEED = 3
 6
 7
 8class Ball:
 9    def __init__(self, position_x, position_y, change_x, change_y, radius, color):
10
11        # Take the parameters of the init function above, and create instance variables out of them.
12        self.position_x = position_x
13        self.position_y = position_y
14        self.change_x = change_x
15        self.change_y = change_y
16        self.radius = radius
17        self.color = color
18
19    def draw(self):
20        """ Draw the balls with the instance variables we have. """
21        arcade.draw_circle_filled(self.position_x, self.position_y, self.radius, self.color)
22
23    def update(self):
24        # Move the ball
25        self.position_y += self.change_y
26        self.position_x += self.change_x
27
28        # See if the ball hit the edge of the screen. If so, change direction
29        if self.position_x < self.radius:
30            self.position_x = self.radius
31
32        if self.position_x > SCREEN_WIDTH - self.radius:
33            self.position_x = SCREEN_WIDTH - self.radius
34
35        if self.position_y < self.radius:
36            self.position_y = self.radius
37
38        if self.position_y > SCREEN_HEIGHT - self.radius:
39            self.position_y = SCREEN_HEIGHT - self.radius
40
41
42class MyGame(arcade.Window):
43
44    def __init__(self, width, height, title):
45
46        # Call the parent class's init function
47        super().__init__(width, height, title)
48
49        # Make the mouse disappear when it is over the window.
50        # So we just see our object, not the pointer.
51        self.set_mouse_visible(False)
52
53        arcade.set_background_color(arcade.color.ASH_GREY)
54
55        # Create our ball
56        self.ball = Ball(50, 50, 0, 0, 15, arcade.color.AUBURN)
57
58    def on_draw(self):
59        """ Called whenever we need to draw the window. """
60        arcade.start_render()
61        self.ball.draw()
62
63    def update(self, delta_time):
64        self.ball.update()
65
66    def on_key_press(self, key, modifiers):
67        """ Called whenever the user presses a key. """
68        if key == arcade.key.LEFT:
69            self.ball.change_x = -MOVEMENT_SPEED
70        elif key == arcade.key.RIGHT:
71            self.ball.change_x = MOVEMENT_SPEED
72        elif key == arcade.key.UP:
73            self.ball.change_y = MOVEMENT_SPEED
74        elif key == arcade.key.DOWN:
75            self.ball.change_y = -MOVEMENT_SPEED
76
77    def on_key_release(self, key, modifiers):
78        """ Called whenever a user releases a key. """
79        if key == arcade.key.LEFT or key == arcade.key.RIGHT:
80            self.ball.change_x = 0
81        elif key == arcade.key.UP or key == arcade.key.DOWN:
82            self.ball.change_y = 0
83
84
85def main():
86    window = MyGame(640, 480, "Drawing Example")
87    arcade.run()
88
89
90main()

19.4. Moving with the Game Controller

Working with game controllers is a bit more complex. A computer might not have any game controllers, or it might have five controllers plugged in.

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 for your application 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()
else:
    print("There are no joysticks.")
    self.joystick = None

19.5. Joystick Values

After this, you can get the position of the game controller joystick by calling self.joystick.x and self.joystick.y.

Try this, combined with the initialization code from above:

move_with_game_controller_print.py
 1import arcade
 2
 3SCREEN_WIDTH = 640
 4SCREEN_HEIGHT = 480
 5MOVEMENT_SPEED = 5
 6DEAD_ZONE = 0.02
 7
 8
 9class Ball:
10    def __init__(self, position_x, position_y, change_x, change_y, radius, color):
11
12        # Take the parameters of the init function above, and create instance variables out of them.
13        self.position_x = position_x
14        self.position_y = position_y
15        self.change_x = change_x
16        self.change_y = change_y
17        self.radius = radius
18        self.color = color
19
20    def draw(self):
21        """ Draw the balls with the instance variables we have. """
22        arcade.draw_circle_filled(self.position_x, self.position_y, self.radius, self.color)
23
24    def update(self):
25        # Move the ball
26        self.position_y += self.change_y
27        self.position_x += self.change_x
28
29        # See if the ball hit the edge of the screen. If so, change direction
30        if self.position_x < self.radius:
31            self.position_x = self.radius
32
33        if self.position_x > SCREEN_WIDTH - self.radius:
34            self.position_x = SCREEN_WIDTH - self.radius
35
36        if self.position_y < self.radius:
37            self.position_y = self.radius
38
39        if self.position_y > SCREEN_HEIGHT - self.radius:
40            self.position_y = SCREEN_HEIGHT - self.radius
41
42
43class MyGame(arcade.Window):
44
45    def __init__(self, width, height, title):
46
47        # Call the parent class's init function
48        super().__init__(width, height, title)
49
50        # Make the mouse disappear when it is over the window.
51        # So we just see our object, not the pointer.
52        self.set_mouse_visible(False)
53
54        arcade.set_background_color(arcade.color.ASH_GREY)
55
56        # Create our ball
57        self.ball = Ball(50, 50, 0, 0, 15, arcade.color.AUBURN)
58
59        # Get a list of all the game controllers that are plugged in
60        joysticks = arcade.get_joysticks()
61
62        # If we have a game controller plugged in, grab it and
63        # make an instance variable out of it.
64        if joysticks:
65            self.joystick = joysticks[0]
66            self.joystick.open()
67        else:
68            print("There are no joysticks.")
69            self.joystick = None
70
71    def on_draw(self):
72
73        """ Called whenever we need to draw the window. """
74        arcade.start_render()
75        self.ball.draw()
76
77    def update(self, delta_time):
78
79        # Update the position according to the game controller
80        if self.joystick:
81            print(self.joystick.x, self.joystick.y)
82
83
84def main():
85    window = MyGame(640, 480, "Drawing Example")
86    arcade.run()
87
88
89main()

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.

../../_images/c.jpg

Centered (0, 0)

../../_images/d.jpg

Down (0, 1)

../../_images/dl.jpg

Down/Left (-1, 1)

../../_images/dr.jpg

Down/Right (1, 1)

../../_images/u.jpg

Up (0, -1)

../../_images/ul.jpg

Up/Left (-1, -1)

../../_images/ur.jpg

Up/Right (1, -1)

We can move the ball by adding the following code to the update:

def update(self, delta_time):

    # Update the position according to the game controller
    if self.joystick:
        print(self.joystick.x, self.joystick.y)

        self.ball.change_x = self.joystick.x
        self.ball.change_y = -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. There’s a long story to that, which I will not bore you with now.

But with this code our ball moves so slow. How do we speed it up? We can make it run five times faster by multiplying by five if we want.

def update(self, delta_time):

    # Update the position according to the game controller
    if self.joystick:
        print(self.joystick.x, self.joystick.y)

        self.ball.change_x = self.joystick.x * 5
        self.ball.change_y = -self.joystick.y * 5

Or better yet, define a constant variable at the top of your program and use that. In our final example below, we’ll do just that.

19.6. Deadzone

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” on a person’s character. We often counteract this by having a “dead zone” where if the number is below a certain value, we just assume it is zero to eliminate the drift.

See the highlighted lines for how we take care of the dead zone:

move_with_game_controller.py
  1import arcade
  2
  3SCREEN_WIDTH = 640
  4SCREEN_HEIGHT = 480
  5MOVEMENT_SPEED = 5
  6DEAD_ZONE = 0.02
  7
  8
  9class Ball:
 10    def __init__(self, position_x, position_y, change_x, change_y, radius, color):
 11
 12        # Take the parameters of the init function above, and create instance variables out of them.
 13        self.position_x = position_x
 14        self.position_y = position_y
 15        self.change_x = change_x
 16        self.change_y = change_y
 17        self.radius = radius
 18        self.color = color
 19
 20    def draw(self):
 21        """ Draw the balls with the instance variables we have. """
 22        arcade.draw_circle_filled(self.position_x, self.position_y, self.radius, self.color)
 23
 24    def update(self):
 25        # Move the ball
 26        self.position_y += self.change_y
 27        self.position_x += self.change_x
 28
 29        # See if the ball hit the edge of the screen. If so, change direction
 30        if self.position_x < self.radius:
 31            self.position_x = self.radius
 32
 33        if self.position_x > SCREEN_WIDTH - self.radius:
 34            self.position_x = SCREEN_WIDTH - self.radius
 35
 36        if self.position_y < self.radius:
 37            self.position_y = self.radius
 38
 39        if self.position_y > SCREEN_HEIGHT - self.radius:
 40            self.position_y = SCREEN_HEIGHT - self.radius
 41
 42
 43class MyGame(arcade.Window):
 44
 45    def __init__(self, width, height, title):
 46
 47        # Call the parent class's init function
 48        super().__init__(width, height, title)
 49
 50        # Make the mouse disappear when it is over the window.
 51        # So we just see our object, not the pointer.
 52        self.set_mouse_visible(False)
 53
 54        arcade.set_background_color(arcade.color.ASH_GREY)
 55
 56        # Create our ball
 57        self.ball = Ball(50, 50, 0, 0, 15, arcade.color.AUBURN)
 58
 59        # Get a list of all the game controllers that are plugged in
 60        joysticks = arcade.get_joysticks()
 61
 62        # If we have a game controller plugged in, grab it and
 63        # make an instance variable out of it.
 64        if joysticks:
 65            self.joystick = joysticks[0]
 66            self.joystick.open()
 67        else:
 68            print("There are no joysticks.")
 69            self.joystick = None
 70
 71    def on_draw(self):
 72
 73        """ Called whenever we need to draw the window. """
 74        arcade.start_render()
 75        self.ball.draw()
 76
 77    def update(self, delta_time):
 78
 79        # Update the position according to the game controller
 80        if self.joystick:
 81
 82            # Set a "dead zone" to prevent drive from a centered joystick
 83            if abs(self.joystick.x) < DEAD_ZONE:
 84                self.ball.change_x = 0
 85            else:
 86                self.ball.change_x = self.joystick.x * MOVEMENT_SPEED
 87
 88            # Set a "dead zone" to prevent drive from a centered joystick
 89            if abs(self.joystick.y) < DEAD_ZONE:
 90                self.ball.change_y = 0
 91            else:
 92                self.ball.change_y = -self.joystick.y * MOVEMENT_SPEED
 93
 94        self.ball.update()
 95
 96
 97def main():
 98    window = MyGame(640, 480, "Drawing Example")
 99    arcade.run()
100
101
102main()