== Developing with Freeform Design Surfaces: GooCanvas and PyGame - Instructors: rickspencer3 == {{{#!irc [18:00] Logs for this session will be available at http://irclogs.ubuntu.com/2011/09/08/%23ubuntu-classroom.html following the conclusion of the session. [18:02] sooo [18:02] hi everybody [18:02] you just missed my best stff [18:03] I was talking in teh wrong channel for the last 2 minutes :) [18:03] so, let's try again ... [18:03] Hello all. Today I will discuss 2 of the APIs that I have used to have a lot of fun with programming for Ubuntnu. [18:03] These APIs are GooCanvas and PyGame. They are both similar in the sense that they provide you with a 2d surface on which you can construct interactive GUIs for your users. [18:03] seriously, I have had tons of fun writing apps with thes over the years [18:04] However, I fonud them to have different strengths and weaknesses. If you choose the correct API it will be more easy and more fun to write yoru app. [18:04] so, why goocanvas or pygame at all? [18:04] A typical desktop app is composed of widgets that a user is used to. Like buttons, entry boxes, and such. [18:04] For these desktop apps, I strongly recommend sticking with PyGtk for the time being. [18:04] like the next year, I think [18:05] PyGtk is the way to go for what I call "boxy" apps [18:05] I use pygtk all the time [18:05] However, sometimes part of an app, or pretty much a whole app, won't need buttons and lists and entry boxes, but will need to display, modify, or animate images, drawings, etc... [18:05] Sometimes in response to user input, sometimes not. [18:05] My goal for this session is to help you choose the right API for those kinds of apps, and to get you started with them. [18:05] please ask questions at any time [18:05] I will check for questions often [18:06] I'll start with GooCanvas because I already did a session on this last year, so there is lots of material. [18:06] https://wiki.ubuntu.com/UbuntuOpportunisticDeveloperWeek/GooCanvas [18:06] basically, I shall copy and past from there, answering quetsions as I go [18:07] thoguh I may skip some to leave room for pygame [18:07] So what is a goocanvas? [18:07] A goocanvas is a 2d composing surface [18:07] You can use it to make pretty much any kind of image [18:08] It's kind of like an api around a drawing program [18:08] So you can have a ton of fun using a goocanvas, because you are pretty much freed from the constraints of a widget library in creating your UI [18:08] goocanvas is cairo under the covers [18:08] and is designed to easily integrate into your gtk app [18:08] So let's add a goocanvas to a pygtk app [18:09] Add it just like a normal pygtk widget [18:09] #set up the goo canvas [18:09] self.goo_canvas = goocanvas.Canvas() self.goo_canvas.set_size_request(640, 480) self.goo_canvas.show() [18:09] tada! [18:09] you have a goocanvas [18:09] Be sure to set the size, otherwise it defaults to 1000,1000, it does not default to the size alloted to it in your window. [18:09] Handle window resizing to resize your goocanvas as well [18:09] !! [18:10] the goocanvas won't automatically change size if it's container changes size [18:10] For example, if your goocanvas is in a VBox, you can do this: [18:10] rect = self.builder.get_object("vbox2").get_allocation() self.goo_canvas.set_bounds(0,0,rect.width,rect.height) [18:10] remember the root item for your goocanvas, you'll need it later often self.root = self.goo_canvas.get_root_item() [18:10] The "root" is like the root of an item tree in XML [18:10] So now that we have a goocanvas, we need to add "Items" to it. [18:10] Anything that can be added to a goocanvas is an Item. It get's it's capabilities by inheriting from ItemSimple, and by implementing the Item interface. [18:11] Let's add an item to the goocanvas to get a look at how it works in general. [18:11] We'll start by adding an image. [18:11] First, you need to get a gtk.pixbux for your image: [18:11] pb = gtk.gdk.pixbuf_new_from_file(path) [18:11] Then you calculate where you want the image to show on the goocanvas. You'll need a top and a left to place most items on a goo canvas. [18:11] For example, to center the image, I do this: [18:11] cont_left, cont_top, cont_right, cont_bottom = self.goo_canvas.get_bounds() img_w = pb.get_width() img_h = pb.get_height() img_left = (cont_right - img_w)/2 img_top = (cont_bottom - img_h)/2 [18:11] it's a bit hard to read, I guess [18:12] but I basically just calculated the pixel center of the goocanvas [18:12] and stored the "bounds" that the calculation returned [18:12] Now I am ready to create the item. [18:12] Note that I create the Item, but there is nothing like goocanvas.add(item) rather, when you create the item, you set it's parent property. [18:12] The parent property is the root of the goocanvas [18:12] This is why I remember the root [18:12] goocanvas.Image(pixbuf=pb,parent=self.root, x=img_left,y=img_top) [18:13] This basic pattern is how you add all other types of items. [18:13] decide where to put the item, and set it's parent property to the root of the goocanvas. [18:13] To remove the item from the goocanvas, you don't tell the goocanvas to remove it [18:13] rather you tell the item to remove itself [18:13] item.remove() [18:13] any questions at all so far? [18:14] In a moment, I'll go on to discuss the types of things that you can add to a goocanvas [18:14] In my mind, there are really 3 types of items [18:14] normal items that you add to draw the stuff you want [18:14] this includes: [18:14] Ellipse, Image, Path, Polyline, Rect, and Text [18:15] the second type is for layout [18:15] Layout and gruop items include: [18:15] Group, Grid, and Table [18:15] then finally, [18:15] there is also Widget. Widget is pretty cool. [18:15] You can add a gtk widget to your goocanvas, but note that it will live in a world seperate from the goocanvas [18:15] In other words, gtk.Widgets won't be rendered if you create images form our goocanvas and such [18:15] However, this is a cool way to add in situ editing to your goocanvas [18:15] We'll just be talking about normal items for the rest of this class though [18:15] So what are some of the things that you do with an item? Well, you compose with it. So you scale it, move it, rotate it, change it's z-order and such [18:16] For a lot of things that you want to do with an item, you use set_property and get_property [18:16] For example, to set the a might make a Text item like this: [18:16] txt = goocanvas.Text(parent=self.root,text="some text", x=100, y=100, fill_color=self.ink_color) [18:16] then change the text in it like this: [18:16] txt.set_property("text","new text") [18:16] Let's look at colors for a moment. There are generally two color properties to work with, stork-color, and fill-color [18:16] If you've ever used a tool ink inkscape, this will make sense you to [18:16] for something like a rect, stroke-color is the outline of the rectangle, and fill-color is the inside of the rectangle [18:17] any questions so far? [18:17] okay, moving on [18:17] You can move, rotate, resize, and skew items [18:17] The APIs for doing this are intuitive, imho [18:17] To grow something by 10% [18:17] item.scale(1.1,1.1) [18:17] And to shrink it a bit: [18:17] item.scale(.9,.9) [18:18] Note that the items always consider themeselves to be their original size and orientation, so doing this will cause an item to grow twice: item.scale(1.1,1.1) item.scale(1.1,1.1) [18:18] Now, when you start rotating and skewing items, some pretty confusing stuff can start happening [18:18] Essentially, an item tracks it's own coordinate system, and doesn't much care about the goocanvas's coordinate system [18:18] So if you rotate an item, for example, the coordinate systems are totally out of whack [18:18] So if you pass the x/ys to an item based on the canvas's coordinate system, it can get waaaay out of whack [18:19] Fortunately, goocanvas has some functions on it that just do these transforms for me [18:19] let's say I catch a mouse click event on an item [18:19] and I want to know where on the item the click happened [18:19] well, the click coordinate are reported in the goocanvas's coordinate system, so I need to do a quick calculation to determine where the click happened on the item: [18:19] e_x, e_y = self.goo_canvas.convert_to_item_space(self.selected_item,event.x,event.y) [18:19] [18:19] so, I used all of these facilities and more to make Photobomb [18:20] you can check out Photobomb if you want to see some of the things that you can do with a GooCanvas [18:20] Photobomb is essentially an image editor [18:20] that made it a good candidate for GooCanvas [18:20] however, I've also written games [18:20] and PyGame is a better API for that [18:21] before I go on to PyGame, any questions on Googcanvas? [18:21] GooCanvas* [18:21] bUbu87 asked: how do you work with svg and gooCanvas? is there a simple way to load an svg to a canvas and keep it scaled all the time? [18:21] indeed! [18:21] there are shapes and paths that are all described with svg [18:22] I've actually exported content from InkScape into a Goocanvas in the past [18:22] let's look at paths and clipping for an example [18:22] A path is essentially a "squiggle" [18:22] It is defiened by a string that gets parsed into x,y coords, and then drawn with a bezier curve formula applied [18:22] ^for those not totally familiar with svg [18:22] here is a string that described a scribble: [18:22] line_data = "M 4.0 4.0C4.0 4.0 5.0 4.0 5.0 4.0 5.0 4.0 6.0 4.0 6.0 3.0 10.0 1.0 13.0 2.0 9.0 15.0 6.0 36.0 28.0 11.0 28.0 11.0 29.0 11.0 33.0 12.0 33.0 15.0 32.0 19.0 27.0 51.0 27.0 53.0 27.0 54.0 27.0 54.0 27.0 54.0 36.0 49.0 37.0 49.0" [18:22] then I can make a path out of this: [18:22] path = goocanvas.Path(data=line_data, parent=self.root, line_width=self.ink_width, stroke_color=self.ink_color) [18:23] so this will draw the path in the goocancas [18:23] Now, a path is also useful because you can use it to clip another object [18:23] You don't use a path object for this, just the string item.set_property("clip-path",line_data) [18:23] shall I move on to PyGame? [18:24] I put the Pygame notes here: [18:24] https://wiki.ubuntu.com/UbuntuOpportunisticDeveloperWeek/PyGame [18:24] PyGame is an API that is also for 2d surfaces. [18:24] It is best for applications where there is a lot of updating of animation without user input (especially as it uses blitting). [18:24] It has a set of baseclasses that make it easier to manage and change teh state of objects. [18:24] It also has collision detection routines, which is very useful in game programming. [18:25] So, net/net if you are doing something that is a game, or game-like, you're likely to want to use pygame, not GooCanvas [18:25] pygame has fairly good reference documentation here: http://pygame.org/docs/ref/index.html [18:25] There are also lots of tutorials available on the web. However, it's important to note that I use pygame a bit differently than they do in the typical tutorials. [18:25] ^^WARNING WARNING^^ [18:25] Tutorials typically have you create a pygame window to display you game in, and then create a loop with a pygame clock object. [18:25] I don't do it this way anymore. Now I prefer to embed a pygame surface into a Gtk app. This has some benefits to me: [18:26] I can use menus for the GUI for things like starting games, pausing etc... [18:26] I can use dialog boxes for things like hight scores or collecting information from users [18:26] If you try to do these things from within a pygame loop, the gtk.main lool clashes with your pygame loop, and everything is just really hard to use. [18:26] So, for the approach I take, I have three samples that you can look at at your leisure: [18:26] 1. sample_game code: [18:26] http://bazaar.launchpad.net/~rick-rickspencer3/+junk/pygame-pygtk-example/view/head:/game.py [18:26] blog posting: [18:26] http://theravingrick.blogspot.com/2011/08/using-pygame-in-pygtk-app.html [18:27] This is the simplest code that I could make to demonstrate how to embed pygame and handle input. [18:27] [18:27] 2. jumper: [18:27] http://bazaar.launchpad.net/~rick-rickspencer3/+junk/jumper/view/head:/jumper/JumperWindow.py [18:27] This is only slightly more complex. It show how animate a sprite by changing the image, and show collision detection and playing a sound. [18:27] [18:27] 3. smashies: [18:27] http://bazaar.launchpad.net/~rick-rickspencer3/+junk/smashies/files/head:/smashies/ [18:27] This is a full blown game which I have almost completed. I'm considering selling it in the software center when I am done. This one handles all the complexity of lives, scores, pausing, etc... [18:27] [18:27] smashies is essentially an asteroids clone [18:27] I'm stilling thinking of a good name, and I need to replace some artwork [18:28] anywho ... [18:28] For this tutorial, we'll focus on jumper since it has an animated Sprite. [18:28] before I dive in, any general questions about PyGame? [18:28] okee let's go [18:28] The overall approach is simple [18:28] 1. set up a drawing area in Gtk Window [18:29] 2. add pygame sprites to it [18:29] 3. handle keyboard input from the gtk window [18:29] 4. periodically call an update function to: [18:29] a. update the data for the sprites [18:29] b. update the view [18:29] c. detect collisions and respond to them [18:30] A game typically needs a background image. I put a background image and the other images and sounds in the data/media directory. Once you get the background image painting, it means you've got the main part of the game set up. So, we'll go through this part with patience. [18:30] ^note that jumper is a Quickly app [18:30] I put all the code in JumperWindow, so it's easy to see in one place. [18:30] let's start making it work [18:30] You need to import 3 modules: [18:30] import pygame [18:30] import os [18:30] import gobject [18:31] You'll see why you need these each in turn. [18:31] First we want to create a a pygame.Image object to hold the background. Once we have that, we can use pygame functions to paint it. === Odd-rationale_ is now known as Odd-rationale [18:31] Since Jumper is a Quickly app, it I can use "get_media_file" to load it. [18:31] I mean load it from the disk [18:31] So I make the background in these 2 lines of code in the finish_initializing function: [18:31] bg_image = get_media_file("background.png") [18:31] self.background = pygame.image.load(bg_image) [18:31] Before I use it, I have to set up the pygame environment though. I do this by adding a gtk.DrawingArea to the gtk.Window, and telling the os module to use the windows xid as a drawing surface. [18:32] You can't just do that in the finish_initializing function, though. This is because drawingarea1 may not actually have an xid yet. This is easy to handle by connecting to the drawing area's "realize" signal. At that point, it will have an xid, and you can set up the environment. [18:32] basically, you need to make sure that the drawingarea has been put on the screen, otherwise, it has no xid [18:32] So, connect to the signal in finish initalizing: [18:32] self.ui.drawingarea1.connect("realize",self.realized) [18:32] and then write the self.realized function: [18:32] def realized(self, widget, data=None): [18:32] os.putenv('SDL_WINDOWID', str(self.ui.drawingarea1.window.xid)) [18:32] pygame.init() [18:32] pygame.display.set_mode((300, 300), 0, 0) [18:32] self.screen = pygame.display.get_surface() [18:33] This function intializes pygame, and also create a pygame.Screen object that you need for drawing. [18:33] so now we have a Gtk.DrawingArea ready to be a PyGame surface [18:33] any questions before I show how to put the game background on it? [18:34] ok, moving on [18:34] So now that the drawing area is set up as a pygame surface, we need to actually draw to it. [18:35] Actually, we'll want to periodically update the drawing so that it appears animated. So we want to update it over and over again. [18:35] So after setting up pygame in tghe realized function, add a gobject timeout to recurringly call a function to update the game: [18:35] gobject.timeout_add(200, self.update_game) [18:35] the funciton update_game will be called every 200 millliseconds. For a real game, you might want to make it update more often. [18:35] So, now we need write the udpate_game function. Eventually it will do a lot more, but for now, it will just tell the game to draw. So we need to write the draw_game function as well. [18:35] def update_game(self): [18:35] self.draw_game() [18:35] return True [18:35] def draw_game(self): [18:35] self.screen.blit(self.background, [0,0]) [18:35] pygame.display.flip() [18:35] Note that update_game returns True. This is important, because if it returns anything else, gobject will stop calling it. [18:36] Looking at draw_game a little more, the first line tells the Screen object to "blit" the background. This means to only update the parts that have changed. [18:36] This keeps the game from flickering on slower systems. We also pass in x/y coordinates to tell it to update the whole background. [18:36] This doesn't paint to the screen yet, though. It just prepares it in memory. [18:36] You can call blit a whole bunch of times for different sprites, but until you call pygame.display.flip() they won't actually be painted to the screen. [18:36] In this way, the screen only gets update once, and the animation is smooth. [18:37] Now if you run the game, you should see the background painted. [18:37] before I go on to animating a sprite, any questions? [18:37] * rickspencer3 drums fingers [18:38] * rickspencer3 scratches head [18:38] * rickspencer3 twiddles thumbs [18:38] ok [18:38] At this point you have a drawing surface set up, and you are drawing to it in a loop. [18:38] Now let's add an animated sprite. [18:38] I put 2 png's in the data/media director. One called "guy1.png" and called "guy2.png". We will animate the game by swapping these images back and forth every time the game paints. [18:38] WARNING: I am doing something very wrong! [18:38] Jumper loads the images as needed from disk. In a real game, this is a bad idea. This is a bad idea because that takes IO time, which can slow the game down. [18:39] DON'T DO IT THIS WAY!!! [18:39] It's better to load all the images and sounds at once when the game loads. See smashies for how I do that in the __init__.py file. [18:39] anyway [18:39] I mentioned before that pygame has some useful base classes. One of those base classes is called "Sprite" which is a really old game programming term. [18:39] When adding an object to your game, it's best to derive from sprite. It's easier to manage the data for a sprite that way, and also there are useful pygame functions that expect a Sprite object. [18:39] So, first create the sprite class and an initialization function: [18:39] class Guy(pygame.sprite.Sprite): [18:39] def __init__(self): [18:39] pygame.sprite.Sprite.__init__(self) [18:39] self.animation_stage = 1 [18:39] self.x = 35 [18:39] self.y = 180 [18:39] self.direction = 0 [18:40] Next, we'll write a function called "update". You'll see in a bit why it's important to call it "update". [18:40] For this function, check which animation stage to use, and then use that image: [18:40] def update(self): [18:40] img = get_media_file("""guy%i.png""" % self.animation_stage) [18:40] self.image = pygame.image.load(img) [18:40] ^remember don't do it like this [18:40] ^load the image from disk once at the beginning fo the program [18:40] Next, you need to set the "rect" for the Sprite. The rect will be used in any collision detection functions you might use: [18:40] self.rect = self.image.get_rect() [18:40] self.rect.x = self.x [18:40] self.rect.y = self.y [18:41] Finally, update the animation stage. [18:41] self.animation_stage += 1 [18:41] if self.animation_stage > 2: [18:41] self.animation_stage = 1 [18:41] Now you just need to a "Guy" to your background. [18:41] First, create a guy in the finish_initializing function. [18:41] self.guy = Guy() [18:41] Since a game will have a lot of sprites, it's easiest to manage sprites as a group [18:41] There is a pygame class for this called a SpriteGroup, which you create by called RenderUpdates. [18:41] So, crate a SpriteGroup and the guy to it: [18:41] self.sprites =pygame.sprite.RenderUpdates() [18:41] self.sprites.add(self.guy) [18:42] Remember when we created the update_game function? [18:42] Now you can see how useful the SpriteGroup is. [18:43] You can call "update" on the sprite group, and it will in turn call update on every sprite in it. So add that call to the update_game function: [18:43] self.sprites.update() [18:43] Now, you also need to tell the Guy to draw. That's easy too with the SpriteGroup. Add this line to draw_game function: [18:43] self.sprites.draw(self.screen) [18:43] Now when you run the game, each tick the guy will swap images, and it will look like it's moving. [18:43] * rickspencer3 phew [18:44] ok, almost done [18:44] any questions? [18:44] WARNING [18:44] Note that I handle keyboard and mouse input very differently than they describe in most pygame tutorials [18:44] Responding to keyboard input is really easy, because you can just use gtk events. [18:44] I have found that you need to attach to the key events for the window, not the drawing area. [18:45] So, to make the guy jump when the user clicks the space bar, I make a key_press_event signal handler, that calls "jump()" on the guy: [18:45] def key_pressed(self, widget, event, data=None): [18:45] if event.keyval == 32: [18:45] self.guy.jump() [18:45] You can look at the jump and update functions in the Guy class to see how a jump was implemented. [18:45] you can track mouse events, key up events, etc.. this way too [18:46] Pygame also has functions for joysticks and stuff, but I haven't used that [18:46] So, that's the essence of creating an animated sprite, which gets you a lot of the way toward making a game. [18:46] We don't have time to delve into everything, but I did want to touch on collisions. [18:46] Assuming that you've added another sprite called self.apple that tries to hit the guy, you can use one of the many pygame collision detection functions in every call to update_game to see if the apple hit the guy: [18:46] if pygame.sprite.collide_rect(self.guy,self.apple): [18:46] self.guy.kill() [18:46] BELIEVE ME [18:46] you don't want to write your own collision detection routines [18:47] there are lots of good functions [18:47] ones that compare whole groups of sprites, for example [18:47] If you set the rect for your Sprite subclass, functions like this work well, and are easy. [18:47] I also mentioned sounds. [18:47] Pygame has a really rich set of sound functions. [18:47] The easiest thing to demo is playing a sound from a file, like this: [18:47] sound_path = get_media_file("beep_1.wav") [18:47] sound = pygame.mixer.Sound(sound_path) [18:47] sound.play() [18:48] .... [18:48] and [18:48] that's everything I prepared for this session [18:48] I'm happy to take some questions [18:48] or maybe everyone is busy playing smashies right now [18:50] There are 10 minutes remaining in the current session. [18:51] thanks ClassBot }}}