python


This implements an iterable root-widget for Albow.   Sometimes the monolithic ‘run’ is fine, sometimes you want your own main loop.    This is basically Greg Ewings .run_modal() code, reorganized as multiple methods.  The methods to call are .run_start() once, .process_input() when relevant events are available,  and .draw_gui() at each frame (or when redrawing is necessary).

from albow import root
from pygame.locals import *
from pygame.event import Event
import pygame

class NewRootWidget(root.RootWidget):

    def __init__(self, surface):
        root.RootWidget.__init__(self, surface)
        self._progen = None
        self.bg_color = None

    def run_modal(self, modal_widget):
        #print 'new root widget'
        self.run_start(modal_widget)
        while True:
            events = [pygame.event.wait()]
            events.extend(pygame.event.get())
            self.process_input(events)
            if redraw:
                self.draw_gui()

    def run_start(self, modal_widget):
        self._progen = self._input_process_generator(modal_widget)
        self._progen.next()

    def process_input(self, events):
        """process mouse and keyboard events.
        @param events: a list or tuple of pygame events
        @returns: True if any widgets need redrawing, otherwise False
        """
        return self._progen.send(events)

    def _input_process_generator(self, modal_widget):
        is_modal = modal_widget is not None
        modal_widget = modal_widget or self

        old_top_widget = root.top_widget
        root.top_widget = modal_widget
        was_modal = modal_widget.is_modal
        modal_widget.is_modal = True
        # modal_widget.modal_result = None
        if not modal_widget.focus_switch:
            modal_widget.tab_to_first()
        # mouse_widget = None
        if root.clicked_widget:
            root.clicked_widget = modal_widget
        num_clicks = 0
        last_click_time = 0
        do_draw = True

        events = []
        while True:
            for event in events:
                type = event.type
                if type == QUIT:
                    self.quit()
                elif type == MOUSEBUTTONDOWN:
                    do_draw = True
                    t = root.get_ticks()
                    if t - last_click_time <= root.double_click_time:
                        num_clicks += 1
                    else:
                        num_clicks = 1
                    last_click_time = t
                    event.dict['num_clicks'] = num_clicks
                    root.add_modifiers(event)
                    mouse_widget = self.find_widget(event.pos)
                    if not mouse_widget.is_inside(modal_widget):
                        mouse_widget = modal_widget
                    root.clicked_widget = mouse_widget
                    root.last_mouse_event_handler = mouse_widget
                    root.last_mouse_event = event
                    mouse_widget.notify_attention_loss()
                    mouse_widget.handle_mouse('mouse_down', event)
                elif type == MOUSEMOTION:
                    root.add_modifiers(event)
                    modal_widget.dispatch_key('mouse_delta', event)
                    mouse_widget = self.find_widget(event.pos)
                    root.last_mouse_event = event
                    if root.clicked_widget:
                        root.last_mouse_event_handler = mouse_widget
                        root.clicked_widget.handle_mouse('mouse_drag', event)
                    else:
                        if not mouse_widget.is_inside(modal_widget):
                            mouse_widget = modal_widget
                        root.last_mouse_event_handler = mouse_widget
                        mouse_widget.handle_mouse('mouse_move', event)
                elif type == MOUSEBUTTONUP:
                    root.add_modifiers(event)
                    do_draw = True
                    #mouse_widget = self.find_widget(event.pos)
                    if root.clicked_widget:
                        root.last_mouse_event_handler = root.clicked_widget
                        root.last_mouse_event = event
                        root.clicked_widget = None
                        root.last_mouse_event_handler.handle_mouse('mouse_up', event)
                elif type == KEYDOWN:
                    key = event.key
                    root.set_modifier(key, True)
                    do_draw = True
                    self.send_key(modal_widget, 'key_down', event)
                    if root.last_mouse_event_handler:
                        event.dict['pos'] = root.last_mouse_event.pos
                        event.dict['local'] = root.last_mouse_event.local
                        root.last_mouse_event_handler.setup_cursor(event)
                elif type == KEYUP:
                    key = event.key
                    root.set_modifier(key, False)
                    do_draw = True
                    self.send_key(modal_widget, 'key_up', event)
                    if root.last_mouse_event_handler:
                        event.dict['pos'] = root.last_mouse_event.pos
                        event.dict['local'] = root.last_mouse_event.local
                        root.last_mouse_event_handler.setup_cursor(event)
                elif type == root.MUSIC_END_EVENT:
                    self.music_end()
                elif type == USEREVENT:
                    root.make_scheduled_calls()
                    if not is_modal:
                        do_draw = self.redraw_every_frame
                        if root.last_mouse_event_handler:
                            event.dict['pos'] = root.last_mouse_event.pos
                            event.dict['local'] = root.last_mouse_event.local
                            root.add_modifiers(event)
                            root.last_mouse_event_handler.setup_cursor(event)
                        self.begin_frame()
            events = yield do_draw

        modal_widget.is_modal = was_modal
        root.top_widget = old_top_widget
        root.clicked_widget = None

    def draw_gui(self):
        if self.is_gl:
            self.gl_clear()
            self.gl_draw_all(self, (0, 0))
        else:
            self.draw_all(self.surface)
        pygame.display.flip()
Advertisements
# As part of the Pitcher's Duel Project, I am conducting a comparative
# analysis of pygame GUI modules, and publishing the results on my blog.
# The comparison consists of implementing the same sample interface on
# each of the various GUIs.
#
# This code implements the interface using the Albow GUI library. For
# details on this library, see:
# http://www.cosc.canterbury.ac.nz/greg.ewing/python/Albow/
#
# The module author is: Gregory Ewing
#
# This source code is the work of David Keeney, dkeeney at travelbyroad dot net
#

# Import Modules
import pygame
from pygame.locals import *
import time
import math

# import gui stuff
from albow import widget, controls, fields
from newroot import NewRootWidget

screenSize = (640, 430)

# define the necessary callback functions.
# these are invoked by the gui widgets
#
def logAction(ed, text):
  """Add the text to the 'edit' window (callback function)"""
  ed.items.append(text)
  ed.text = '\n'.join(ed.items[-10:])
  ed.invalidate() # mark as dirty

# these classes extend the Albow classes slightly, to
# add writing to the log widget
class LoggingRadioButton(controls.RadioButton, controls.ButtonBase):
    pass

class LoggingCheckBox(controls.CheckBox, controls.ButtonBase):
    pass

class LoggingTextField(fields.TextField, controls.ButtonBase):
    def __init__(self, width=None, **kws):
        fields.TextField.__init__(self, width, **kws)
        self._prevText = self.text

    def attention_lost(self):
        if self.text != self._prevText:
            self.action()
            self._prevText = self.text

widget.Widget.fg_color = (10,0,10)
controls.Button.fg_color = (100,75,0)

# the body of the program is here
#
def main():
  """This function is called when the program starts. It initializes
  everything it needs, then runs in a loop until the function returns.
  """

  #Initialize Everything
  pygame.init()
  screen = pygame.display.set_mode(screenSize)
  pygame.display.set_caption('GUI Test - Albow')
  pygame.time.set_timer(pygame.USEREVENT, 200)

  # create GUI object
  gui = NewRootWidget(screen)
  gui.run_start(None)

  # create page label
  lbl = controls.Label('Pygame GUI Test Page - Albow', fg_color=(75,150,0))
  lbl.topleft = 29, 13
  gui.add(lbl)

  # create progress bar label
  #  progress bar itself is not implemented
  lbl4 = controls.Label('Progress Bar')
  lbl4.topleft = 356, 355
  gui.add(lbl4)

  # create edit box
  ed = controls.Label('\n'.join(('',)*15), width=250)
  ed.border_color = (10,0,10)
  ed.border_width = 1
  ed.margin = 15
  ed.topleft = 377, 22
  gui.add(ed)
  ed.items = [] #(250, 320)
  logAction(ed,'top line of input')
  logAction(ed,'second line of input')
  ed.text = '\n'.join(ed.items[-10:])

  # create CheckBoxes and add to gui
  cb1Val = [False,True,None]
  cb1 = LoggingCheckBox()
  cb1.action = lambda: logAction(ed, 'Check Box #1 clicked %s'%cb1.ref.get())
  cb1.ref = controls.ItemRef(cb1Val,0)
  cb1.topleft = 42, 45
  cb1L = controls.Label('Check box #1')
  cb1L.topleft = 62, 45
  gui.add((cb1, cb1L))
  cb2 = LoggingCheckBox()
  cb2.action = lambda: logAction(ed, 'Check Box #2 clicked %s'%cb2.ref.get())
  cb2.ref = controls.ItemRef(cb1Val,1)
  cb2.topleft = 42, 75
  cb2L = controls.Label('Check box #2')
  cb2L.topleft = 62, 75
  gui.add((cb2, cb2L))
  cb3 = LoggingCheckBox()
  cb3.action = lambda: logAction(ed, 'Check Box #3 clicked %s'%cb3.ref.get())
  cb3.topleft = 42, 103
  cb3.ref = controls.ItemRef(cb1Val,2)
  cb3L = controls.Label('Check box #3')
  cb3L.topleft = 62, 103
  gui.add((cb3, cb3L))

  # create radio buttons, put in table, and add to gui
  rb1Val = [None]
  rb1Ref = controls.ItemRef(rb1Val,0)
  rb1 = LoggingRadioButton()
  rb1.ref = rb1Ref
  rb1.setting = 1
  rb1.topleft = 200, 45
  rb1.action = lambda: logAction(ed, 'Radio Button #1 clicked %s'%rb1.ref.get())
  rb1L = controls.Label('Radio Button #1')
  rb1L.topleft = 220, 45
  gui.add((rb1, rb1L))
  rb2 = LoggingRadioButton()
  rb2.ref = rb1Ref
  rb2.setting = 2
  rb2.topleft = 200, 75
  rb2.action = lambda: logAction(ed, 'Radio Button #2 clicked %s'%rb2.ref.get())
  rb2L = controls.Label('Radio Button #2')
  rb2L.topleft = 220, 75
  gui.add((rb2, rb2L))
  rb3 = LoggingRadioButton()
  rb3.topleft = 200, 103
  rb3.ref = rb1Ref
  rb3.setting = 3
  rb3.action = lambda: logAction(ed, 'Radio Button #3 clicked %s'%rb3.ref.get())
  rb3L = controls.Label('Radio Button #3')
  rb3L.topleft = 220, 103
  gui.add((rb3, rb3L))

  # create txt box label
  lbl2 = controls.Label('Text Box')
  lbl2.topleft = 31, 145
  gui.add(lbl2)
  # create text box
  en = LoggingTextField('                                ')
  en.border_color = (0,0,0)
  en.topleft = 31, 170
  en.action = lambda: logAction(ed, 'Text field value changed %d'%len(en.text))
  gui.add(en)

  # add button
  btn1 = controls.Button('Button 1', action=lambda: logAction(ed,'Button 1 Clicked'))
  btn1.topleft = 30, 250
  gui.add(btn1)
  btn2 = controls.Button('Button 2', action=lambda: logAction(ed,'Button 2 Clicked'))
  btn2.topleft = 130, 250
  gui.add(btn2)
  btn3 = controls.Button('Button 3', action=lambda: logAction(ed,'Button 3 Clicked'))
  btn3.topleft = 30, 290
  gui.add(btn3)
  btn4 = controls.Button('Button 4', action=lambda: logAction(ed,'Button 4 Clicked'))
  btn4.topleft = 130, 290
  gui.add(btn4)

  # add image map
  imap = controls.ImageButton("clear.png", action=lambda: logAction(ed, 'ImageMap Clicked '))
  imap.topleft = 31, 340
  gui.add(imap)

  # make some insensitive
  btn2.enabled = False
  cb3.enabled = False

  #Main Loop
  while 1:

    #Handle Input Events
    for event in pygame.event.get():
      if event.type == QUIT:
          return
      elif event.type == KEYDOWN and event.key == K_ESCAPE:
          return

      # pass event to gui
      gui.process_input((event,))

      # clear background, and draw clock-spinner
      screen.fill((250, 250, 250))
      radius = 30
      spinPos = 240, 362
      sp2 = spinPos[0]+1, spinPos[1]
      progressAngle = int(time.time() % 15 * 24 - 90) #60
      pygame.draw.circle(screen, (180, 180, 180), spinPos, radius, 0)
      for angle in range(-90, progressAngle):
        a = angle*math.pi/180
        tgt = radius*math.cos(a)+spinPos[0], radius*math.sin(a)+spinPos[1]
        pygame.draw.line(screen, (254,254,254), spinPos, tgt, 2)
        pygame.draw.circle(screen, (0,0,0), spinPos, radius, 2)
        pygame.draw.circle(screen, (0,0,0), spinPos, radius+1, 3)
        pygame.draw.circle(screen, (0,0,0), sp2, radius, 2)
        pygame.draw.circle(screen, (0,0,0), sp2, radius+1, 3)
        pygame.draw.line(screen, (0,0,0), spinPos, tgt, 2)
        tgt = spinPos[0], spinPos[1]-radius
        pygame.draw.line(screen, (0,0,0), spinPos, tgt, 2)

      # Draw GUI
      gui.draw_gui()

      pygame.display.flip()

main()

Edited to include ButtonBase, and uncomment cb3.enabled line.

Years ago, I started posting reviews here of various pygame GUIs, and reviewed 3 or 4.

My own choice was OcempGUI, but have recently fallen out with it.   I have been looking at Albow as a replacement candidate, and implemented my comparison app in it.

To start, I had to write a new RootWidget that allows iterative operation,  called per-frame from my own loop.  That sub-class RootWidget will be included here in a later post.

Albow has a decent collection of widgets; the only widgets my app wanterd that weren’t included were a progress-bar and toggle-buttons.  The progress bar could be implemented fairly easily, and the toggle-button is just a different look for a check-box, so neither omission is a great loss.

Only the Button widget was complete, for my purposes.  Other widgets, I had to sub-class to add the logging feature.  It was necessary to sub-class, incorporating the ButtonBase mixin, to add an ‘action’ attribute calling the logging function.   None of these sub-classing chores were difficult, as the module is fairly straightforward to understand and classes easy to derive from.   I was able to disable the check-box and radio button widgets,  but they showed no outward sign of being disabled.  The ‘action’ attribute just stopped working.

Styling Albow widgets was as easy as any library I have reviewed.   Just set border_color and fg_color attributes to 3-tuple colors, and it’s good.

The code for the app is in the next post, and the NewRootWidget is in the post after that.  Feel free to use any as you wish.

This weekend was mildly productive.  I resolved the memory leak in Pitchers Duel, which was actually a memory leak in Lamina.   So I put a corrected version of Lamina in subversion; I also updated the demos to fail more politely in the absence of necessary GUI libraries.

I will try to get a new release packaged up and put on Pypi this week, but the current code is available at the link below.

/branches/Lamina/Pitchers Duel

David

This subclass of Twisted’s selectreactor has methods to yield control of the reactor to its caller, and resume control later.

The guts of mainLoop have been moved to a generator method, and mainLoop instantiates the generator and calls .next() on it.

There are new methods for new functionality.

.release() causes the mainLoop to yield at the end of its current iteration.

.resume() transfers control back into the reactor. A .resume call is like .run, in that it does not yield control until the next .release or .stop.

.isRunning() tells you whether the reactor is running.



from twisted.python import log, failure, util
from twisted.internet.selectreactor import SelectReactor

class PausingReactor(SelectReactor):
    """Implement selectreactor that can be exited and resumed. """

    def __init__(self, *args):
        SelectReactor.__init__(self, *args)
        self._releaseRequested = False
        self._mainLoopGen = None

    def _mainLoopGenerater(self):
        """Generater that acts as mainLoop, but yields when
        requested.
        """
        while self.running:
            try:
                while self.running:
                    # Advance simulation time in delayed event
                    # processors.
                    self.runUntilCurrent()
                    t2 = self.timeout()
                    t = self.running and t2
                    self.doIteration(t)

                    if self._releaseRequested:
                        self._releaseRequested = False
                        yield None
            except:
                log.msg("Unexpected error in main loop.")
                log.deferr()
            else:
                log.msg('Main loop terminated.')

    def mainLoop(self):
        """Setup mainLoop generater, and initiate looping. """
        self._mainLoopGen = self._mainLoopGenerater()
        self.resume()

    def resume(self):
        """Resume mainLoop looping after interruption. """
        try:
            self._mainLoopGen.next()
        except StopIteration:
            pass

    def release(self):
        """Request main loop pause and reacter yield to caller. """
        self._releaseRequested = True

    def isRunning(self):
        """Is reactor running? """
        return self.running

def install():
    """Configure the twisted mainloop to be run using the pausingreactor.
    """
    reactor = PausingReactor()
    from twisted.internet.main import installReactor
    installReactor(reactor)

__all__ = ['install']

To use the module,
import pausingreactor; pausingreactor.install()

The pausingreactor module, and a couple of files to test it, are linked. The stresstest.py sends increasing long strings to an echo type server ‘uppercaser_ns.py’, reads them back and checks them for intactness. It also pauses a couple times per second for a fraction of a second.

pausingreactor.py

stresstest.py

uppercaser_ns.py

Enjoy.

I have been considering networking options for months, and over the last couple of weeks have had time to explore specific options. I initially made exploratory forays into two disparate implementations: One approach was to write the game logic in ‘inside-out’ form so that everything could be done by callback; the other approach was to put the network code into another process, and pipe data to and from it via os pipes.

The ‘inside-out’ approach would add significant burden to coding the game logic, and was abandoned after an evenings work. The separate process approach was making some headway, but I have put it aside as well, for yet another approach.

What I want(1) is a twisted reactor that can be exited and reentered. I could then use the substantial asset of proven twisted code for various high and low level operations, but use a conventional subroutine-oriented code structure. When an engine needs to exit, it can have the reactor yield to its caller; the caller then follows game logic to set up another engine, which resumes the reactor operation. The timing constraints would be the same as for a call-back, (ie: don’t delay the reactor more than necessary), but the game logic would be more readable and more writable without having to put everything in callbacks.

The usage would be like:

 
class Engine() 
   def iterate(self): 
       # do per frame stuff 
       if self._done:  reactor.release() # yield to engine's caller 
       else:   reactor.callLater(0, self.iterate) 
   def run(self): 
       # init stuff 
       reactor.callLater(0, self.iterate) 
       if reactor.isRunning(): reactor.resume() 
       else:  reactor.run()    

def intro(): 
   # set up intro pages 
   eng = Engine() 
   eng.add(intro_pages) 
   eng.run()   

def room(room_context): 
   eng = Engine() 
   eng.add(room_context) 
   eng.run() 
   return eng.retval   

def main(): 
   intro() 
   # set up lobby context 
   next = room(lobby_context) 
   while next != GAME_EXIT: 
        # set up context for next room 
        next = room(room_context)           

The code is fairly readable, with more specialized function in specialized subroutines.  Calls to the engine can be from main(),  or from a subroutine a few calls deep.  

See my next post for the reactor subclass itself.

1)
OK, what I really want is a file-like connection object:
con = Connection(‘gameserver.org’)
con.write(‘game update data’, game_data)
news = con.read()
check = con.read_non_blocking()
But I am not going to find (or write) that, so I’ll settle for an pause-able reactor.

APRESS.COM : Beginning Game Development with Python and Pygame: From Novice to Professional

by Will McGugan

This book seems well fitted for a beginner, as it starts with a basic Python tutorial, and steps up gradually to more sophisticated material. The book is more useful as a tutorial, describing a tank game with increasing complexity, from interactive text to 2D animation, to 3D in OpenGL, than as a reference.

The chapter titles are meaningful, but task oriented, ‘making things move’, ‘user input’, …. ‘making things go boom’. Once you’ve identified the chapter that might answer your question, you have to skim to find the material, though the material does have abundant subheadings, at least one per page, to help you in skimming. There is a nice index of 20 pages, which seemed thorough, in that the half dozen items I looked up were all there.

Will covers a pretty good range of topics, including packaging, sound, openGL, artificial intelligence, and inputs. The only section that surprised me in its presence was the ‘Moving into the third dimension’, in that he discusses the calculations involved in mapping 3D space to the 2D display. Most 3D programmers, I suspect, just offload that calculation burden onto the OpenGL (or DirectX) engine, and do not do such translations ourselves. Maybe the skills might be useful for people who want 3D effects in straight pygame, and the chapter may serve as a transition from the 2D material presented prior, to the OpenGL material to follow. I just skimmed through briefly, as I was already familiar with OpenGL.

The only substantial omission I found was that networking was not addressed. He may consider the networking options too unsettled at this point to be able to recommend a specific approach.

I was disappointed to not find any advice on determining which joystick button or mouse button is which. Pygame recognizes buttons 1, 2, 3, etc.., but there is no firm rule on which number belongs to a given button on the input device. The right mouse button is button 2, or button 3, depending on whether scroll-wheel is present, and I don’t know a systematic way to determine what buttons the user’s mouse has, and what numbers thy hold. Maybe this problem doesn’t have a clear resolution.

I cant complete a review of a book on game programming in python without a comparison with the other book on the topic. Sean Riley’s ‘Game Programming with Python’. GPwP is a bigger, more involved work, that is less tied to Pygame, and less approachable for beginners. If you are a beginner looking to ease your learning curve, get BGDwP. If your question isn’t answered by BGDwP, it might be by GPwP. For what it’s worth, ‘Game Programming with Python’ does discuss networking, but did not help me with my joystick button interpretation.

Next Page »