Skip to content

Instantly share code, notes, and snippets.

@rpuntaie
Last active December 2, 2020 12:05
Show Gist options
  • Save rpuntaie/5674cccf0ba6eaa5337e96f69349923d to your computer and use it in GitHub Desktop.
Save rpuntaie/5674cccf0ba6eaa5337e96f69349923d to your computer and use it in GitHub Desktop.
Loop pygame in separate thread to play with graphics REPL style (similar to processing.js)

It is a great idea to use graphics and multimedia to introduce students to programming. In the JVM world there is Processing. There is also a Javascript version p5js.

To me Python seems easier as a first contact language. It does not need compilation and no large virtual machine (like JVM in Processing). There are good REPL's (IPython, Jupyter) and a vast selection of libraries. Read-Eval-Print-Loop's allow a fast feedback when adjusting a specific detail. In combination with an editor that forwards lines to the interpreter in a terminal (vim,vscode), this is interactive coding. It can be used in combination with a debugger that stops at a specific context necessary for the detail.

So I was looking for a Python alternative to Processing.

There is a Jython based processing.py, but it still needs JVM and does not integrate well with IPython or Jupyter.

There is the promissing CPython based p5py. I could not make it work from a REPL, though.

There is turtle.py, which is part of CPython. It can be used from a REPL. It works well to play with code for beginners.

Another great address for graphics enthusiast python learners is blender.

I tried [pygame](clone https://github.com/pygame/pygame). It keeps the window open if running the loop from a separate thread. Then one can use the API interactively from a REPL and/or editor and see the immediate effect of parameters and function changes. It can be used in combination with pymunk.

The pygameprocessing.py module here places the pygame loop in a separate thread, to allow fledgling coders to play with Python code.

"""
pygameprocessing.py: Loop pygame in separate thread to play REPL style.
@gfun: gdraw, kd_ (key down), ku_ (up), md_ (mouse down), mu_ (up), mw_ (wheel)
mouse: BUTTON_ {LEFT,MIDDLE,RIGHT,WHEELDOWN,WHEELUP,X1,X2}
keys: K_
0,...,a,...,F1,...,LEFT,RIGHT,UP,DOWN,PAGEDOWN,PAGEUP,INSERT,DELETE,HOME,END,SPACE,
KP0,...,KP_DIVIDE,KP_ENTER,KP_EQUALS,KP_MINUS,KP_MULTIPLY,KP_PERIOD,KP_PLUS,
AMPERSAND,ASTERISK,AT,BACKQUOTE,BACKSLASH,BACKSPACE,BREAK,
CAPSLOCK,CARET,CLEAR,COLON,COMMA,CURRENCYSUBUNIT,CURRENCYUNIT,DOLLAR,
EQUALS,ESCAPE,EURO,EXCLAIM,GREATER,HASH,HELP,LALT,LCTRL,LEFTBRACKET,
LEFTPAREN,LESS,LGUI,LMETA,LSHIFT,LSUPER,MENU,MINUS,MODE,NUMLOCK,NUMLOCKCLEAR,
PAUSE,PERCENT,PERIOD,PLUS,POWER,PRINT,PRINTSCREEN,QUESTION,QUOTE,QUOTEDBL,
RALT,RCTRL,RETURN,RGUI,RIGHTBRACKET,RIGHTPAREN,RMETA,RSHIFT,RSUPER,
SCROLLLOCK,SCROLLOCK,SEMICOLON,SLASH,SYSREQ,TAB,UNDERSCORE,UNKNOWN
help(circle)
circle(surface, color, center, radius, width=0, draw_top_right=None, draw_top_left=None, draw_bottom_left=None, draw_bottom_right=None) -> Rect
help(arc)
arc(surface, color, rect, start_angle, stop_angle, width=1) -> Rect
help(ellipse)
ellipse(surface, color, rect, width=0) -> Rect
help(rect)
rect(surface, color, rect, width=0, border_radius=0, border_top_left_radius=-1, border_top_right_radius=-1, border_bottom_left_radius=-1, border_bottom_right_radius=-1) -> Rect
help(polygon)
polygon(surface, color, points) -> Rect
help(Color)
Color('#FF0EEE'); Color(0xFF0EEE); Color((92,92,200))
help(Surface)
pygame object for representing images
Surface((width, height), flags=0, depth=0, masks=None) -> Surface
help(transform.rotate)
rotate(Surface, angle) -> Surface
help(gscreen.blit)
blit(source, dest, area=None, special_flags=0) -> Rect
help(gscreen.set_clip)
set_clip(rect) -> None
help(image.load)
img = image.load(filename)
gscreen.blit(img,(0,0))
help(font.render)
render(text, antialias, color, background=None) -> Surface
help(mixer.Sound)
sound = mixer.Sound("data/whiff.wav")
sound.play(-1) #-1 loop
sound.stop()
Example1:
.. code::
from pygameprocessing import *
ginit((600,600))
gscreen.fill([30]*3)
U = gwidth//9
p=(300,233)
circle(gscreen,"RED",p,radius=U)
circle(gscreen,"VIOLET",p,radius=U,width=U//2)
p=(433,233)
circle(gscreen,"RED",p,radius=U)
circle(gscreen,"VIOLET",p,radius=U,width=U//2)
gquit()
Example2:
.. code::
from pygameprocessing import *
import os
#ginit((720,720))
img = image.load(os.path.expanduser('~/Downloads/green.jpg'))
run = ginit(img.get_size())
#These two lines work on Linux, but not on Windows (see below)
from threading import Thread
Thread(target=run).start()
class Chimp(sprite.Sprite):
def __init__(self):
super().__init__()
self.image = image.load("data/chimp.bmp")
self.image.set_colorkey(self.image.get_at((0, 0)), RLEACCEL)
self.rect = self.image.get_rect()
def update(self):
pos = mouse.get_pos()
self.rect.midtop = pos
class Eye(Surface):
def __init__(self):
self.R = 100
super().__init__([2*self.R]*2, SRCALPHA, 32)
self.Vx,self.Vy = (0,0)
self.X,self.Y = gcenter
def __call__(self):
circle(self,"BLUE",[self.R]*2,radius=self.R)
circle(self,"WHITE",[self.R]*2,radius=self.R,width=self.R//3)
self.X,self.Y = self.X + self.Vx*gsec, self.Y+self.Vy*gsec
gscreen.blit(self,(self.X-self.R,self.Y-self.R))
def accelerate(self,dvx,dvy):
self.Vx,self.Vy = self.Vx + dvx, self.Vy + dvy
class KeyText:
def __init__(self):
self.keytext = font.Font(None, 36).render("hjkl move, q quit", 1, 'red')
def __call__(self):
textpos = self.keytext.get_rect(centerx=gwidth/2)
gscreen.blit(self.keytext, textpos)
eye = Eye()
keytext = KeyText()
chimp = sprite.Group((Chimp(),))
@gfun
def gdraw(): # about gFPS=60 times/s
#Global.gdraw = None # to undo and call function individually
gscreen.blit(img,(0,0))
eye()
keytext()
chimp.draw(gscreen)
@gfun
def md_BUTTON_LEFT(e):
chimp.update()
dV = 10
@gfun
def kd_K_h(e): eye.accelerate(-dV,0)
@gfun
def kd_K_l(e): eye.accelerate(dV,0)
@gfun
def kd_K_j(e): eye.accelerate(0,dV)
@gfun
def kd_K_k(e): eye.accelerate(0,-dV)
sound = mixer.Sound("data/whiff.wav")
sound.play(-1) #-1 loop
@gfun
def kd_K_q(e):
gquit()
sound.stop()
#In Windows CLI and GUI loop in parallel not possible
#So instead of the separate thread above do
#run() # in main thread
'''
sound.stop()
gquit()
gfunsdel()
'''
"""
import pygame
from pygame import *
from pygame.display import *
from pygame.draw import *
import sys
import time
_pygame = dir(pygame)
_keys = {eval(x):x for x in _pygame if x.startswith('K_')}
_mouse = {eval(x):x for x in _pygame if x.startswith('BUTTON_')}
class _global_injector:
def __setattr__(self,name,value):
sys.modules['builtins'].__dict__[name] = value
def __getattr__(self,name):
return sys.modules['builtins'].__dict__[name] if name in sys.modules['builtins'].__dict__ else None
def update(self,d):
sys.modules['builtins'].__dict__.update(d)
Global = _global_injector()
gfuns=set()
def gfun(f):
gfuns.add(f.__name__)
setattr(Global,f.__name__,f)
def gfunsdel():
for f in gfuns:
setattr(Global,f,None)
def ginit(size):
init()
font.init()
mixer.init()
Global.gscreen = set_mode(size)
Global.gcenter = (size[0]/2,size[1]/2)
Global.gwidth = size[0]
Global.gheight = size[1]
_quit = False
def gquit():
nonlocal _quit
_quit = True
Global.gquit = gquit
Global.gabssec = time.perf_counter()
Global.gFPS = 60
def _safecall(funname,*a):
if fun := getattr(Global,funname):
try:
fun(*a)
except Exception as exc:
setattr(Global,funname,None)
print('removed %s because %s'%(funname,exc))
fpsClock = pygame.time.Clock()
def _run():
while not _quit:
try:
for e in event.get():
if e.type == QUIT:
quit()
sys.exit()
if e.type == KEYDOWN:
_safecall('kd_%s'%_keys[e.key],e)
elif e.type == KEYUP:
_safecall('ku_%s'%_keys[e.key],e)
elif e.type == MOUSEBUTTONDOWN:
_safecall('md_%s'%_mouse[e.button],e)
elif e.type == MOUSEBUTTONUP:
_safecall('mu_%s'%_mouse[e.button],e)
elif e.type == MOUSEWHEEL:
_safecall('mw_%s'%_mouse[e.button],e)
fpsClock.tick(gFPS)
tgsec = time.perf_counter()
if tgsec > gabssec:
Global.gsec = tgsec - gabssec
Global.gabssec = tgsec
_safecall('gdraw')
flip()
except Exception as e:
print(e)
break
quit()
return _run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment