Tuesday, May 22, 2012

Making of an Arcade Frontend - Part 4

Previous entries:
Prologue
Part 1
Part 2
Part 3

Last part I talked about some boring math used to create a pretty rotating list of games. This time I'll be showing some more code. The goal today will be to have a smooth transition between game titles on the list.

So, here's what we're starting with. The code to position and draw the rotating games menu. No special effects and no transitions, the selected game is in the middle. Don't forget to read the comments since they cover most of the "clever" parts of the function.

# NOTE: These functions are members of a class.
# I've omitted most of it since it's not relevant to the example


def _selectorPos( self, i ):
    # Here's the math that was discussed in the previous blog!
    x = self.radius * math.sin( float( i ) * 3.14159 / float( len( self.selectors ) - 1 ) )
    y = (i - self.selectedIndex) * self.spacing
    return (x, y)

def Draw( self, screen ):
    # Draw the top half of the menu
    for i in range( self.selectedIndex ):
        pos = add(self.center, self._selectorPos( i ))
        # Rect is a tuple of vectors
        # The first is the top left, the second is the bottom right
        rect = (add(self.pos, neg(self.unselectedSize)), add(self.pos, self.unselectedSize))
        self.selectors[i].Draw( screen, rect )

    # Draw the bottom half of the menu
    # Reverse order so items closer to the center are drawn on top of ones further away
    for i in range( len( self.selectors ) - 1, self.selectedIndex, -1 ):
        pos = add(self.center, self._selectorPos( i ))
        rect = (add(self.pos, neg(self.unselectedSize)), add(self.pos, self.unselectedSize))
        self.selectors[i].Draw( screen, rect )

    # Draw the currently selected item, last so it's on top
    pos = add(self.center, self._selectorPos( selectedIndex ))
    rect = (add(pos, neg(selectedSize)), add(pos, selectedSize))
    self.selectors[selectedIndex].Draw( screen, rect )

Notice I've hidden some math away in functions. This makes the purpose of this code easier to discern since it's not buried in a ton of repetitive arithmetic. The functions add and neg are 2D vector addition and negation. Since Python provides tuples as a built in type I used them to represent vectors. This can be bad for performance because every time I do math on vectors a new vector gets created. That means lots of garbage objects for the garbage collector to clean up. Optimization is as easy as replacing tuples with a mutable type and would have no effect on this code since all the real work is hidden in functions. I haven't done so here because it hasn't been an issue.

So if we use this code to display a menu it will display a list of games in a nice circle with the selected game in the middle at one size, and everything else above and below it at a different size. Items further from the center are displayed behind ones closer. This allows items to overlap if I want and still look reasonably good.

Now, if you remember the skeleton I showed in Part 2, I had some code for responding to input. Now it's time to update it to allow the selected game to change. New stuff is in italics

# the main loop!
frameStart = time.clock()
time.sleep( 0.01 ) # XXX: don't want a frame time of 0 when starting
while True:
    frameTime = time.clock() - frameStart
    frameStart = time.clock()
        
    for event in pygame.event.get():
        if event.type == pygame.QUIT or (event.type == KEYDOWN and event.key == pygame.K_F12):
            sys.exit()
        elif event.type == KEYDOWN:
            if not selector.InTransition():
                if event.key == pygame.K_DOWN:
                    selector.Next()
                elif event.key == pygame.K_UP:
                    selector.Previous()
        
    selector.Update(frameTime)
    selector.Draw(screen)
    pygame.display.flip()
    screen.fill( (0,0,0) )

Nothing too surprising I hope! The only really special code here is right after the new KEYDOWN check. The checks simply make sure the person using the launcher can't change the selected game while the selection is in the process of changing. The code for selector.Next and selector.Previous is about the same. It sets a flag in the selector object to make it change selections. The work of changing selections happens in Update and Draw. Update maintains a timer which lasts for about 1/5th of a second. It also stops the timer and changes the selected game once the timer expires. Draw handles positioning everything based on the value of the timer. If no transition is happening the drawing code I showed you at the top is run. Otherwise some different code runs...

alpha = self.timer / self.transitionTime

# Draw the top half of the menu
for i in range( self.selectedIndex ):
    pos = add(self.centre, blend(self._selectorPos( i ), self._selectorPos( i - 1 ), alpha))
    rect = (add(pos, neg(self.unselectedSize)), add(pos, self.unselectedSize))
    self.selectors[i].Draw( screen, rect )

# Draw the bottom half of the menu
for i in range( len( self.selectors ) - 1, self.selectedIndex - 1, -1 ):
    pos = add(self.centre, blend(self._selectorPos( i ), self._selectorPos( i - 1 ), alpha))
    rect = (add(pos, neg(self.unselectedSize)), add(pos, self.unselectedSize))
    self.selectors[i].Draw( screen, rect )

# Draw the old selection
pos = add(self.centre, blend(self._selectorPos( self.selectedIndex ), self._selectorPos( self.selectedIndex - 1 ), alpha))
rect = (add(pos, neg(blend(self.selectedSize, self.unselectedSize, alpha))), add(pos, blend(self.selectedSize, self.unselectedSize, alpha)))
self.selectors[self.selectedIndex].Draw( screen, rect )

# Draw the new selection
pos = add(self.centre, blend(self._selectorPos( self.selectedIndex + 1 ), self._selectorPos( self.selectedIndex ), alpha))
rect = (add(pos, neg(blend(self.unselectedSize, self.selectedSize, alpha))), add(pos, blend(self.unselectedSize, self.selectedSize, alpha)))
self.selectors[self.selectedIndex + 1].Draw( screen, rect )

There's quite a bit of new stuff here. There are a bunch of calls to a new function "blend." What it does is create a new position that's "alpha" percent between two positions. 0% is at the first position, 100% is at the second position, 50% is right between the two. There are three variations on how blend is used.

blend(self._selectorPos( i ), self._selectorPos( i - 1 ), alpha)
This makes the items rotate upward. The top item goes offscreen and disappears, the item below the selection becomes the selected item and a new item slides onscreen from the bottom.
blend(self.selectedSize, self.unselectedSize, alpha)
blend(self.unselectedSize, self.selectedSize, alpha)
These make the old selection go from the size it has when selected to the size of everything else and the new selection go to its selected size.

No comments:

Post a Comment