Thursday, April 18, 2013

Parallel Pong on Raspberry Pis

When building a cluster computer, you need software to run on it. We thought that games would be a great demonstration and this lead us to embark on making the greatest game to ever come to distributed programming, pong.

Everyone needs one of these

I decided that pygame would be a good place to start. The pis come with the python and the module already installed and a simple Google search of “pygame pong” landed hundreds of open source results. I picked a version that was written well and begun to to split the game into two parts, the game logic and the rendering logic.

The main logic ending being a bit tricky to do. Pygame makes creating games easy but it came at a cost for us. The original idea was to leave the main logic mostly unchanged and just have it play the game at 9600x3600 (the resolution of our 5x3 display wall). Unfortunately the game bounds were set by the display module in pygame. If you tried to pass in a resolution that is higher then the screen, it will default to the screen resolution. It wasn't really a fault of pygame, this sort of logic makes sense in most applications, I was just trying to do something unusual with it. To combat this I ended up taking out all of pygame in the game logic. This meant a lose of collision detection, coordinate systems, easy control listeners, image processing, and pretty much everything that would make this a straightforward. To minimize coding, I ended up just writing a coordinate system and collision detection that mimicked pygame's own system, so most of the game logic only had to be changed to call my functions not pygame's. It wasn't as robust but it was sufficient for what I was trying to do. When I was done, we had a version of pong that could be played at any resolution but didn't actually render anything to the screen.

While those changes were important to get the code running how I wanted it, I needed to get the game node to communicating with the rendering nodes. Python provides a simple way to do that, sockets. 
 
    ip_addresses = ( '10.10.0.10', '10.10.0.11', '10.10.0.12', '10.10.0.13',\
                     '10.10.0.14', '10.10.0.15' )
    for x in range(0,len( clisocket )):
        clisocket[x] = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        clisocket[x].connect((ip_addresses[x], 20000))

In 5 lines, I now have open connections to all 6 raspberry pis powering a 3x2(we don't have enough pis for 5x3....yet) display wall. Once we get more pis, the top line will most likely read the addresses from a text file to make managing more addresses easier. For sockets you can see that all you do is create the object, then give it an ip address and port number and say connect. You now have a interface to comunicate with anything on your network. Using the sockets to pass data is also easy and can be done elegantly.

 
    while game.running:
        game.update()
        posvect = struct.pack('iiii', game.ball.position_vec[0], game.ball.position_vec[1], \
            game.paddle_left.rect.y, game.paddle_right.rect.y )
        # loop over clients and send the coordinates
        for x in range( 0,len( clisocket ) ):
            clisocket[x].sendall( posvect ) 
        # wait for them to send stuff back to avoid a race condition.
       for x in range( 0,len( clisocket ) ):
            clisocket[x].recv( 16 )

After the game is updated, the current position of all the important graphics are packaged into a binary byte array and sent to every socket. The game then stops and waits for a confirmation message from the rendering pis to say that they finished their job drawing the graphics and the game can now continue.

The pis that are responsible for drawing the graphics are just small broadcast servers. Again, python makes this extremely easy and is achieved simply by defining a broadcast server class. The broadcast server takes advantage of two super classes. The threadingmixin class allows a server to be asynchronous on a small scale relatively easy. While this particular implementation doesn't take advantage of the threading in an interesting way(it was mainly used so you can have a separate thread listening for esc to end the server manually) it will be expanded upon in the next project as we hope to turn this specific pong render code to a much more general one that can be used with almost any game. Keeping the server in a thread will allow the pi to do other things, like contribute to the main game calculation, while it is waiting to receive the coordinates of the graphics. The TCPserver class allows it to have all the basic server functions in python. This sort of class inheritance made the actual code of the broadcast server easy, as the only work needed was to combine two classes into one.
 
class broadcastServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass

The TCPServer constructor requires a handler class with at the very least a handle function. The handle function is where the magic of pong happens.
 
def handle(self):
        global screen, ball, ballrect, paddle_left_rect, paddle_right_rect, bounds,\
            edge_node, paddle_index
        posvec=self.request.recv(16)
        while posvec !='':
            pos = struct.unpack( 'iiii',posvec )
            screen.fill( black )
            if ( pos[0] > boundsx[0] and pos[0] < boundsx[1] ):
                ballrect.x = pos[0] - boundsx[0] # offset the bounds
                ballrect.y = pos[1] - boundsy[0] 
                screen.blit( ball, ballrect )
            if edge_node:
                if ( pos[paddle_index] > boundsy[0] and pos[paddle_index] < boundsy[1]  ):
                    paddle_rect.y = pos[paddle_index] - boundsy[0]
                    screen.blit( paddle, paddle_rect )
            pygame.display.flip()
            try:
                self.request.send( 'Got it' )
                posvec=self.request.recv( 16 )
            except:
                posvec=''
                print( 'client disconnect' )

The actual rendering is pretty easy once we get the coordinates, it simply checks if the position of the ball and paddles are in the bounds the render node is responsible for. If so, move the ball and draw it, if not, just draw black. The most interesting part about this code is actually the while loop. The while loop runs as long as posvec(position vector) isn't empty. Posvec is set to empty when the game logic node disconnects and the recv or send function throws an exception. The while loop is there because of the way the TCP server in python handles requests. Once a request is made by a socket and the handle function is run in it's entirety, it disconnects from the socket. Rather then constantly make a new conection every time the game updates, which would require first deleting the old sockets then making new ones and finally reconnecting them, the while loop keeps the server in the handle function until nothing is sent or received, thus keeping an open connection.

Everything is actually in a carefully planned space despite wires everywhere
The controls were done differently than just keyboard input, read our previous post. The source code is available on github.

Productivity seems to have gone down lately for some reason....


No comments:

Post a Comment