Moving the Paddles - Keyboard Input - Demo 04

Purpose

Add movement to the paddles using keyboard input.

Demo 01

Demo 04

How to Execute

On Linux or on MacOS, in a shell, type “python src/demo04/demo.py”. On Windows, in a command prompt, type “python src\demo04\demo.py”.

Move the Paddles using the Keyboard

Keyboard Input

Action

w

Move Left Paddle Up

s

Move Left Paddle Down

k

Move Right Paddle Down

i

Move Right Paddle Up

Paddles which don’t move are quite boring. Let’s make them move up or down by getting keyboard input.

And while we are at it, let’s go ahead and create data structures for a Vertex, and for the collection of vertices that make up a Paddle.

Code

Data Structures

Here we use dataclasses, which automatically creates on the class a constructor, accessor methods, and pretty-printer. This saves a lot of boiler plate code.

src/demo04/demo.py
104@dataclass
105class Vertex:
106    x: float
107    y: float
108
109
src/demo04/demo.py
114@dataclass
115class Paddle:
116    vertices: list[Vertex]
117    r: float
118    g: float
119    b: float
120
121

Although Python is a dynamically-typed language, we can add type information as helpful hints to the reader, and for use with static type-checking tools for Python, such as mypy.

src/demo04/demo.py
125paddle1 = Paddle(
126    vertices=[
127        Vertex(x=-1.0, y=-0.3),
128        Vertex(x=-0.8, y=-0.3),
129        Vertex(x=-0.8, y=0.3),
130        Vertex(x=-1.0, y=0.3),
131    ],
132    r=0.578123,
133    g=0.0,
134    b=1.0,
135)
136
137paddle2 = Paddle(
138    vertices=[Vertex(0.8, -0.3), Vertex(1.0, -0.3), Vertex(1.0, 0.3), Vertex(0.8, 0.3)],
139    r=1.0,
140    g=1.0,
141    b=0.0,
142)
  • Create two instances of a Paddle.

I make heavy use of keyword arguments in Python.

Notice that I am nesting the constructors. I could have instead have written the construction of paddle1 like this:

x = -1.0
y = -0.3
vertex_one = Vertex(x, y)
x = -0.8
y = -0.3
vertex_two = Vertex(x, y)
x = -0.8
y = 0.3
vertex_three = Vertex(x, y)
x = -1.0
y = 0.3
vertex_four = Vertex(x, y)
vertex_list = list(vertex_one, vertex_two, vertex_three, vertex_four)
r = 0.57
g = 0.0
b = 1.0
paddle1 = Paddle(vertex_list, r, g, b)

But then I would have many local variables, some of whose values change frequently over time, and most of which are single use variables. By nesting the constructors as the author has done above, the author minimizes those issues at the expense of requiring a degree on non-linear reading of the code, which gets easy with practice.

Query User Input and Use It To Animate

src/demo04/demo.py
147def handle_movement_of_paddles() -> None:
148    global paddle1, paddle2
149    if glfw.get_key(window, glfw.KEY_S) == glfw.PRESS:
150        for v in paddle1.vertices:
151            v.y -= 0.1
152    if glfw.get_key(window, glfw.KEY_W) == glfw.PRESS:
153        for v in paddle1.vertices:
154            v.y += 0.1
155    if glfw.get_key(window, glfw.KEY_K) == glfw.PRESS:
156        for v in paddle2.vertices:
157            v.y -= 0.1
158    if glfw.get_key(window, glfw.KEY_I) == glfw.PRESS:
159        for v in paddle2.vertices:
160            v.y += 0.1
161
162
  • If the user presses ‘s’ this frame, subtract 0.1 from the y component of each of the vertices in the paddle. If the key continues to be held down over time, this value will continue to decrease.

  • If the user presses ‘w’ this frame, add 0.1 more to the y component of each of the vertices in the paddle

  • If the user presses ‘k’ this frame, subtract .1.

  • If the user presses ‘i’ this frame, add .1 more.

  • when writing to global variables within a nested scope, you need to declare their scope as global at the top of the nested scope. (technically it is not a global variable, it is local to the current python module, but the point remains)

The Event Loop

Monitors can have variable frame-rates, and in order to ensure that movement is consistent across different monitors, we choose to only flush the screen at 60 hertz (frames per second).

src/demo04/demo.py
166TARGET_FRAMERATE: int = 60
167
168time_at_beginning_of_previous_frame: float = glfw.get_time()
src/demo04/demo.py
172while not glfw.window_should_close(window):
173    while (
174        glfw.get_time() < time_at_beginning_of_previous_frame + 1.0 / TARGET_FRAMERATE
175    ):
176        pass
177
178    time_at_beginning_of_previous_frame = glfw.get_time()
src/demo04/demo.py
182    glfw.poll_events()
183
184    width, height = glfw.get_framebuffer_size(window)
185    glViewport(0, 0, width, height)
186    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
src/demo04/demo.py
190    draw_in_square_viewport()
src/demo04/demo.py
194    handle_movement_of_paddles()
  • We’re still near the beginning of the event loop, and we haven’t drawn the paddles yet. So we call the function to query the user input, which will also modify the vertices’ values if there was input.

src/demo04/demo.py
198    glColor3f(paddle1.r, paddle1.g, paddle1.b)
199
200    glBegin(GL_QUADS)
201    for vertex in paddle1.vertices:
202        glVertex2f(vertex.x, vertex.y)
203    glEnd()
  • While rendering, we now loop over the vertices of the paddle. The paddles may be displaced from their original position that was hard-coded, as the callback may have updated the values based off of the user input.

  • When glVertex is now called, we are not directly passing numbers into it, but instead we are getting the numbers from the data structures of Paddle and its associated vertices.

Adding input offset

Adding input offset to Paddle 1

src/demo04/demo.py
207    glColor3f(paddle2.r, paddle2.g, paddle2.b)
208
209    glBegin(GL_QUADS)
210    for vertex in paddle2.vertices:
211        glVertex2f(vertex.x, vertex.y)
212    glEnd()
Adding input offset to Paddle 1

Adding input offset to Paddle 2

src/demo04/demo.py
216    glfw.swap_buffers(window)