teaching machines

FourLords Part I

At last week’s Buster meeting, we decided to implement a multi-pilayer version of Warlords, an Atari game that formed my young mind. I’m going to call it FourLords, because really the game doesn’t involve armies or anything on a war-scale. Plus, I want to stress the multiple Pis, multiple players aspect of the game. I drafted three initial steps to get a prototype up and running:

  1. Write a Rectangle class.
  2. Write a bouncing ball renderer.
  3. Write a collision-aware renderer.

1. Write a Rectangle class

Rectangles will be used for the bouncing ball, the forts, and the ultra-sensitive kings hidden away inside the forts. They also need to be movable and able to be checked for collisions with other rectangles.

class Rectangle:
  def __init__(self, l, r, b, t):
    self.l = l
    self.r = r
    self.b = b
    self.t = t

  def collidesWith(self, other):
    return not (self.r < other.l or self.l > other.r or self.t < other.b or self.b > other.t)

  def getVertices(self):
    return [self.l, self.b, 0.0,
            self.r, self.b, 0.0,
            self.l, self.t, 0.0,
            self.r, self.t, 0.0]

  def translate(self, dx, dy):
    self.l += dx
    self.r += dx
    self.b += dy
    self.t += dy

2. Write a bouncing ball renderer

This is the obligatory first step to any game.

The code behind it is a modification of our previous OpenGL renderers:

#!/usr/bin/env python

import time
import sys

from pyopengles import *
from geometry import *
from glop import *

WIDTH = 1680
HEIGHT = 1050
ASPECT = WIDTH / float(HEIGHT)

class FourLordsRenderer(Renderer):
  def __init__(self):
    Renderer.__init__(self)
    self.context = EGL(depth_size=8)
    self.startTime = time.time()

    self.vertexShader = """
      uniform mat4 projection;
      uniform mat4 modelview;

      attribute vec3 position;
      attribute vec3 normal;

      void main() {
        gl_Position = projection * modelview * vec4(position, 1.0);
      }
    """

    self.fragmentShader = """
      precision mediump float;

      uniform float time;
      uniform vec2 resolution;

      void main() {
        gl_FragColor = vec4(vec3(0), 1.0);
      }
    """

    self.binding = ((0, 'position'),)
    self.program = self.context.get_program(self.vertexShader, self.fragmentShader, self.binding)

    opengles.glUseProgram(self.program)
    self.modelviewUID = opengles.glGetUniformLocation(self.program, "modelview")
    self.projectionUID = opengles.glGetUniformLocation(self.program, "projection")
    self.resolutionUID = opengles.glGetUniformLocation(self.program, "resolution")
    self.timeUID = opengles.glGetUniformLocation(self.program, "time")
    opengles.glUseProgram(0)

    opengles.glUseProgram(self.program)
    opengles.glUniform2f(self.resolutionUID, eglfloat(WIDTH), eglfloat(HEIGHT))
    opengles.glUseProgram(0)

    opengles.glClearColor(eglfloat(0.85), eglfloat(0.35), eglfloat(0.15), eglfloat(1.0))

    # Let's make a new rectangle centered in the viewport with a random velocity.
    self.rectangle = Rectangle(-0.1, 0.1, -0.1, 0.1)
    self.velocity = Vector2.getRandom(1.0)

    self.vertices = eglfloats(self.rectangle.getVertices())
    self.checkError('after init')

    self.modelview = Matrix4.getIdentity()

    # Let's always show at least [-1, 1] x [-1, 1]. If our aspect ratio gives
    # us more real estate than that on either x or y, let's increase the span
    # shown on that axis.

    # If we have more height than width, show more on the y-axis.
    if ASPECT < 1.0:
      self.right = 1.0
      self.top = 1.0 / ASPECT

    # Otherwise, show more of the x-axis.
    else:
      self.right = 1.0 * ASPECT
      self.top = 1.0

    self.left = -self.right
    self.bottom = -self.top
    self.projection = Matrix4.getOrthoMatrix(self.left, self.right, self.bottom, self.top, -50.0, 50.0)

  def onKeyEvent(self, keycode):
    pass

  def onMouseEvent(self, event):
    pass

  def onDraw(self):
    opengles.glViewport(0, 0, self.context.width, self.context.height);
    opengles.glClear(GL_COLOR_BUFFER_BIT)

    opengles.glUseProgram(self.program)

    opengles.glUniform1f(self.timeUID, eglfloat(time.time() - self.startTime))
    opengles.glUniformMatrix4fv(self.modelviewUID, 1, GL_FALSE, eglfloats(self.modelview.data))
    opengles.glUniformMatrix4fv(self.projectionUID, 1, GL_FALSE, eglfloats(self.projection.data))

    opengles.glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, self.vertices)
    opengles.glEnableVertexAttribArray(0)

    opengles.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
    opengles.glUseProgram(0)

    self.checkError('after draw')

    openegl.eglSwapBuffers(self.context.display, self.context.surface)

    # Update rectangle's position based on its velocity and the time elapsed
    # since the last frame.
    self.rectangle.translate(self.deltaTime * self.velocity[0], self.deltaTime * self.velocity[1])
    self.vertices = eglfloats(self.rectangle.getVertices())

    # If we exceed the bounds of the visible space, let's reflect the velocity
    # vector about the normal of the boundary we crossed.
    if self.rectangle.l < self.left or self.rectangle.r > self.right:
      self.velocity[0] = -self.velocity[0]
    if self.rectangle.b < self.bottom or self.rectangle.t > self.top:
      self.velocity[1] = -self.velocity[1]

  def checkError(self, msg):
    e = opengles.glGetError()
    if e:
      print msg, 'Error: ', hex(e)
      sys.exit(1)

if __name__ == "__main__":
  with FourLordsRenderer() as renderer:
    render.go(WIDTH, HEIGHT, 0.02, renderer)

To store velocity, I also created a simple Vector2 class:

import math
import random

class Vector2:
  def __init__(self, x = 0.0, y = 0.0):
    self.coords = [x, y]

  def getLength(self):
    return math.sqrt(self.coords[0] * self.coords[0] + self.coords[1] * self.coords[1])

  def normalize(self):
    length = self.getLength()
    if length > 0.0:
      self.coords[0] /= length
      self.coords[1] /= length

  def __getitem__(self, i):
    return self.coords[i]

  def __setitem__(self, i, value):
    self.coords[i] = value

  def __str__(self):
    return repr(self.coords)

  @staticmethod
  def getRandom(magnitude = 1.0):
    angle = random.random() * 2.0 * math.pi
    return Vector2(math.cos(angle) * magnitude, math.sin(angle) * magnitude)

I’m finding that I do not appreciate Python’s mandatory requirements of including the invoking object in methods’ parameter lists and qualifying instance variable references with that object. Python provides a very thin veneer of object-orientation. The customary notation is only seen on method calls. Everywhere else methods look just like functions taking in structs. It doesn’t capitalize on the context benefits that a class provides. Most of my variable accesses are going to be to instance variables. Can’t we make the most common thing the easiest thing? Outlaw shadows of instance variables, require qualification for access to globals, and let’s eliminate qualified access to instance variables. I feel like I’m writing C code, not object-oriented code.

3. Write a collision-aware renderer

The next step is to write a renderer that bounces the ball around a field strewn with randomly-placed rectangles, which disappear when collided with. This one’s not done yet. I need to think about the best way to dynamically remove elements from the vertex array that won’t impose a linear time operation to reshuffle the array’s contents. Probably swapping out the last item in the list for the one collided with will work.

This is work for another day.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *