Hello and welcome.
The basic thing to understand when thinking about porting your CodeSkulptor code over to Python and PyGame is that the vast majority of the code will migrate over with no changes or fairly minor changes. I say this up front so
that you can know that you're not going to have to rewrite large chunks of code from the ground up. Most of the time, if a change is needed it will simply be in the form of replacing a call to
a simpleGUI function with a call to the PyGame equivalent function. Rest assured that you will find an equivalent function in PyGame -- it may be called a bit diferently or behave slightly differently,
but if you are familiar with simpleGUI calls it will likely seem eerily familiar.
Having said that, there are a few differences that I will try to explain as best as I can. One of the main differences is that we need to create our own while()
loop which runs continually while the program
is running (until the program is ended by the user) -- in effect, this keeps our event queue running, our draw handler drawing and the program "on." I'll get into the specifics later. Another main difference is in how
rendering is handled in PyGame: in PyGame, we need to be more explicit in our instructions to the renderer in telling it to refresh our scene with new images while discarding old images.
Again, I'll get into the specifics later. Okay, let's begin.
The first thing you need is Python and PyGame installed on your computer. So, head off to Python to grab the latest release of Python and PyGame to grab the latest matching release of PyGame. Install Python and then Pygame. You should now be ready to begin. So, run IDLE from your Python menu, choose "file --> new window" and from the new window that appears choose "file --> save as" choosing a location and filename. I would choose a location away from the main Python install directory for simplicity sake to keep everything related to your project tidy and isolated. If you are using images and sounds, then it's best to keep all of that stuff away from the main Python install path. Also, don't forget to save with a ".py" extension. So, save as "my_project.py" to directory "c:\cs_port\my_project", for example. You now have your .py file ready for use -- this is equivalent to a blank CodeSkulptor window.
We begin with our imports as normal -- excluding simplegui, of course. We also need to include PyGame and Operating System speific imports:
# import modules import os import pygame # pygame specific locals/constants from pygame.locals import * # some resource related warnings if not pygame.font: print('Warning, fonts disabled') if not pygame.mixer: print('Warning, sound disabled')You can copy the above into the top of your file and also include the other libraries your CodeSkulptor code uses. That's essentially PyGame ready for use except for a call to intialize the library. I don't really know exactly what the call does, but it doesn't really matter from our perspective -- we'll just trust PyGame needs to be initialzed and this is how it's done:
# initializations pygame.init()The next thing we need to do is create a window for our program to run in -- we can think of this like a frame and canvas in CodeSkulptor:
# a bit similar to CodeSkulptor frame creation -- we'll call the window the canvas canvas = pygame.display.set_mode((640, 480)) pygame.display.set_caption("My_Project")The parameters for the display are its size and the caption sets the window title. I've named the display variable "canvas" here for similarity to CodeSkulptor since quite often we will be making calls to draw on it like we do in CodeSkulptor -- elsewhere, often times you'll see it called the screen.
I have two helper functions I pulled from another tutorial and updated to work with Python 3. They are for loading images and
loading sounds and perform some sanity checks, generate error meesages and do file path checking. You can just copy them in after the previous section:
# Pygame Wrapper functions -- resource loading sanity checks # Taken from the "Monkey tutorial" and updated for 3.3 by me # # load Image: # A colorkey is used in graphics to represent a color of the image # that is transparent (r, g, b). -1 = top left pixel colour is used. def load_image(name, colorkey=None): fullname = os.path.join('data\images', name) try: image = pygame.image.load(fullname) except pygame.error as message: print('Cannot load image:', name) raise SystemExit(message) if colorkey is not None: image = image.convert() if colorkey is -1: colorkey = image.get_at((0,0)) image.set_colorkey(colorkey, RLEACCEL) else: image = image.convert_alpha() return image, image.get_rect() # Load Sound def load_sound(name): class NoneSound: def play(self): pass if not pygame.mixer: return NoneSound() fullname = os.path.join('data\sounds', name) try: sound = pygame.mixer.Sound(fullname) except pygame.error as message: print('Cannot load sound:', name) raise SystemExit(message) return soundYou can change the
os.path.join('data\images', name)
and os.path.join('data\sounds', name)
to point where you want. These currently say, "look where the .py file is being run
from and then go to data\images or data\sounds beneath that location for the files."
PyGame requires us to create font and colour objects before they can be used. So, I found it best to create them here along with all of the other Pygame specific code we're adding:
# need to create fonts and colour objects in PyGame #fontObj = pygame.font.Font('ARBERKLEY.ttf', 32) #fontObj2 = pygame.font.Font('ARBERKLEY.ttf', 24) fontObj3 = pygame.font.Font(pygame.font.match_font('timesnewroman'), 32) gold_color = pygame.Color(255, 215, 0) white_color = pygame.Color(255, 255, 255)These are just examples of the ones I needed, so you can create whatever you might need or leave them as placeholders until you see what you might need. The fonts files are loaded without a path, so I'm loading them from the same directory that the .py file is in. I've commented out the calls to specific font files since it will generate an error if the file can not be found. Each font size is a separate object, so you need an object for each size you will need even if it's the same font. There is also the
pygame.font.match_font()
function you can call passing in a font name and it will look for it on the system,
so that function call can be substituted for the string font filename above. That's how fontObj3 is getting its font. You can call pygame.font.get_fonts()
in IDLE and it will return a list of all the
fonts available (hint, then call .sort() on the list). If it can't find the font, then a default one is used. If you plan to distribute your project, then I recommend you do as above in the first two cases and
actually provide the font file along with the code and other resources -- unless you know your font will be available on all systems. Times New Roman, for example, is not installed on Linux by default. So,
fontObj3 should use the PyGame default font when this is run on a Linux machine that has not installed the font.
Okay, that should complete everything we need to do for the top of our file and we should be clear to start copying over our CodeSkulptor code following on. However, let's just hold up on that task for the moment and move to the bottom of the file. Add in a good number of line breaks to give us a bit of distance and then create a function defintion like the following:
# call this function to start everything # could be thought of as the implemntation of the CodeSkulptor frame .start() method. def main(): # initialize loop until quit variable running = True # create our FPS timer clock clock = pygame.time.Clock() #---------------------------Frame is now Running----------------------------------------- # doing the infinte loop until quit -- the game is running while running: # event queue iteration for event in pygame.event.get(): # window GUI ('x' the window) if event.type == pygame.QUIT: running = False # input - key and mouse event handlers elif event.type == pygame.MOUSEBUTTONDOWN: pass # just respond to left mouse clicks #if pygame.mouse.get_pressed()[0]: #mc_handler(pygame.mouse.get_pos()) elif event.type == pygame.KEYDOWN: pass #kd_handler(event.key) # timers #elif event.type == timer_example: #t_example() # the call to the draw handler draw_handler(canvas) # FPS limit to 60 -- essentially, setting the draw handler timing # it micro pauses so while loop only runs 60 times a second max. clock.tick(60) #-----------------------------Frame Stops------------------------------------------ # quit game -- we're now allowed to hit the quit call pygame.quit ()This is one of the places we differ from CodeSkulptor in that we need to actually write the function that "makes things go." In CodeSkulptor we just call
frame.start()
and the draw handler starts drawing,
the event handlers start looking for events, etc. In PyGame, we need to be more explicit and write a function main()
that will accomplish this for us. The meat of this function is an infinite
(until exited by the user) while loop. So, we initialize a boolean "running" to True and then create a while loop that runs as long as that boolean is True. Within this loop we iterate over an event queue
using for event in pygame.event.get()
and then check for the event type and execute some code based on that event -- here is where we execute our CodeSkulptor functions we created as our
various event handlers. We can call those same event handler functions from our CodeSkulptor code to handle the same events in PyGame. We may need to tweak the code in those handlers, but hopefully you
can see how the structure of the events is the same -- we're just doing a bit of extra work to explicitly check for the events on a continual basis, but once we have an event we deal with it in the same
way we do in CodeSkulptor -- with our event handlers.pygame.time.Clock()
after the boolean intialization creates a clock which gets set to 60 frames per second at the bottom of the while loop -- this keeps the while loop from running beyond that limit and is also set to 60
to match the rate at which the CodeSkulptor renderer runs at (recall the CodeSkulptor draw handler runs 60 times per second). An indeed, right above clock.tick(60)
we can see our call to
our draw_handler(canvas)
in PyGame. Again, we just need to go one step further by explicitly calling the draw handler function in PyGame, but once we call it our CodeSkulptor function
and code will do the same job as it did in CodeSkulptor -- we will need to tweak some function calls, but the basic structure remains the same.main()
function we created above. We can do that
with the following bit of code:
# this calls the 'main' function when this script is executed # could be thought of as a call to frame.start() of sorts if __name__ == '__main__': main()As the comment indicates, this can be likened to a
frame.start()
call in CodeSkulptor. The only difference is we've had to write the contents of the function that .start()
represents in
the form of our main()
function above. So, copy the code into your file as the last line. It simply calls the main()
function when the script is run.
Okay, we are now very close to having a skeleton file that we can actually run and have it create something like a frame with canvas in CodeSkulptor terms. If you're anything like me, then you want to
be able to draw something on the canvas so you can actually see that you can accomplish the same thing in PyGame that you can in CodeSkulptor. We're already calling draw_handler(canvas)
in our main()
function above, so if we can implement it then we can see about getting something drawn.
This is the other main difference in PyGame -- we must be more explicit in our instructions when drawing. The first thing to understand is that once we render something onto a surface it stays there.
This took me a long time to get my head around because I'm used to making a call to draw something and having it appear and then stopping that call and having it disappear. This is not quite so
in PyGame -- in PyGame, it's like you have a blackboard and everytime you draw on it, or on top of other things already drawn, the only way to remove that item you've drawn is to erase the whole area
and redraw only the things you didn't want to erase. I know, this seems weird, but this is how it works. We know the draw handler runs at 60 frames per second, so the eye doesn't catch this process,
but that's what's going on. With that in mind, let's look at simple draw handler implementation:
count = 0 draw_colour = white_color def draw_handler(canvas): # clear canvas -- fill canvas with uniform colour, then draw everything below. # this removes everything previously drawn and refreshes canvas.fill((0, 0, 0)) # draw example global count count += 1 text_draw = fontObj3.render("CodeSkulptor Port", True, draw_colour) text_draw2 = fontObj3.render("Tutorial", True, draw_colour) if count % 90 < 45: canvas.blit(text_draw, (190, 220)) else: canvas.blit(text_draw2, (250, 220)) # update the display pygame.display.update()I've included a couple global variables so I can do an example of a simple animation to help illustrate how rendering works. If you paste this code block into your file, above the
main()
function,
for example, you should be able to now run it from within IDLE and have a window with some animating text appear. Choose "run --> run module" from the menu at the top of the code window. Or you can use
the provided skeleton.py file. A nice simple text animation, but we now have something happening.canvas.fill()
which is something we never saw in CodeSkulptor.
This fill operation does just what it sounds like it might do -- it fills the entire screen with a colour. We're using black. This is the equivalent of our blackboard erasure I mentioned earlier.
We wipe everything away that was previously drawn as our first task in the draw handler. We then go on to draw what we need to draw on this refresh and end by calling pygame.display.update()
which completes our draw operation. To see why we need to do this simply comment out the call to the fill method and then run the code again. Strange, isn't it? We're no longer wiping away what was
previously drawn so things just get drawn on top of other things creating a mess. I should add that this is the most inefficient way of clearing away and updating the screen we can use. The smart
and efficient way to do it is to only wipe and update the parts of the screen that have changed since the last refresh -- there's no point in wiping away and redrawing something that hasn't changed. However,
this is the easiset way to do it. Don't expect to be able port graphically intensive games like Rice Rocks using this draw method -- it will grind to a halt. For that you want to look into the sprite
class and the concept of "dirty sprites." This is something I'm just looking at myself and is therefore beyond the scope of this tutorial. I thought it worth mentioning, though..render()
method passing in the text, a Boolean for Anti-Aliasing
(smoothing) and one of our previously created colours. We then come across the .blit()
method called from the canvas. This is way we draw things in PyGame -- the .blit()
method does the actual "putting on screen" -- as you can see it takes an object to draw and an x, y coordinate location for where to draw it. We do the same thing when drawing images -- we make the
same call, but instead of passing a text object we pass an image object we have loaded previously using the supplied load_image()
function. It's important to note that PyGame draws using
the upper left (0, 0) corner of the object and not the center of the object like CodeSkulptor does. This means when porting your code over you will need to adjust locations of draw (.blit()
)
calls to account for the difference. Finally, we have a call to pygame.display.update()
which actually updates the canvas (screen) with all of our changes for this refresh cycle.
Timers are a fair bit different in Pygame. They have no .start() and .stop()
methods, for example. They are simply running when they are given a time of something greater than 0 and
stopped when they are given a time of 0. Also, they hold a user defined event number that is given to them upon creation which is then used when iterating over the event queue to determine which timer
event is happening. As was seen above in the main()
implementation I commented out the handling of the example timer, but you can see what's happening -- when the event is found there
is then a call to the timer handler function as we normally would do in CodeSkulptor. In the elif statement I'm simply checking against a number I created for the timer. Here's timer creation code:
# pygame has no start() and stop() methods -- 0 time is off any other value is on # set some on/off constants for readability with each timer TIMER_OFF = 0 # timer for example -- 1500 milliseconds when on TIMER_EXAMPLE_ON = 1500 # set the timer name to its user event for readability timer_example = USEREVENT + 1 pygame.time.set_timer(timer_example, TIMER_EXAMPLE_ON)I create a constant to turn timers off in the form of
TIMER_OFF
, so I pass that to the timer when I want it stopped. TIMER_EXAMPLE_ON
is set to the rate at which I wish
the timer to fire -- just like in CodeSkulptor. However, I must pass this to the timer function each time I want to start it running. timer_example
is set to USEREVENT + 1
which
simply adds one to the constant which holds where the user events numbers begin (it's in the 30's, I think). This is giving our timer an unique I.D. so it can be picked up on the event queue and then
the proper timer handler function can be called. If you want to experiment with timers you can uncomment the example in the main()
loop, copy the above code into your file just above the
main()
loop implementation and then above that define the timer handler by copying in the code below:
def t_example(): global draw_colour if draw_colour == white_color: draw_colour = gold_color else: draw_colour = white_colorCalls to start and stop this timer look like the code below:
pygame.time.set_timer(timer_example, TIMER_OFF) pygame.time.set_timer(timer_example, TIMER_EXAMPLE_ON)Notice we don't create a variable and assign it to the timer -- we simply call the timer function and pass it a user event number and a time of 0 or more -- the constants just make it more obvious what's going on.
You're free to remove the globals "count" and "draw_colour" as well as everything between canvas.fill()
and pygame.display.update()
in the draw handler function implementation now as
it was just there for illustrative purposes. Those two calls (canvas.fill()
and pygame.display.update()
), however, form the "bookends" of the draw handler and need to remain. If you
have a background image that fills the entire canvas, then you can replace the canvas.fill()
call with a .blit()
of that image which will do the same thing -- remove everything
previously drawn, but replace it with the background image instead of a colour. You then go on and draw the rest of your content following on up to the pygame.display.update()
call.
I'll try and go through some of the pitfalls and equivalent/similar functions I've encountered so far.
print
is a function in Python 3.x -- it must be called thusly print()
note, note_rect = load_image('note_b.png')
The second is the image area rectangle and is used for "dirty sprite" updates. The load_image()
function I provided also returns a rectangle in readiness for using dirty sprites, so we need to provide a second variable name for the rectangle.pygame.transform.scale()
pygame.transform.rotate(image, angle)
will rotate the image the opposite direction than in CodeSkulptor -- that is, a negative angle value yields a CLOCKwise rotation. Image rotation can be a chore..blit)
from the top left corner -- not the center!.fill()
on the entire screen is terribly inefficient. Graphically intense games will choke. Investigate "dirty sprites."# rotations are a real pain -- got this bit of code from PyGame site # ("image" is a previously loaded image) orig_rect = image.get_rect() # note this converts the angle to radians--you might just need to pass the angle instead--also, negative values rotate COUNTERclockwise, so I'm negating to reverse that behaviour rot_image = pygame.transform.rotate(image, - (180 * angle / math.pi)) rot_rect = orig_rect.copy() rot_rect.center = rot_image.get_rect().center rot_image = rot_image.subsurface(rot_rect).copy()
canvas.draw()
is canvas.blit()
in PyGame.play(), .set_volume()
methods, etc. Use stop instead of rewind, though..blit()
only a section of an image just as in CodeSkulptor (e.g. as in Blackjack cards all on one image). See my card class in bjack.py.while()
loop code above. Just replace simpleGUI key constants with the PyGame ones in your key handler (see bjack.py).