I am not going to go through the specifics of setting up Python/Pyglet/IDE’s and ecs, but here are the links to all the appropriate sources:
- Python (I’m using 2.7)
- ecs (I used pip to install it, here is an easy install for pip)
- Pyglet I am using the development version (pyglet 1.2 alpha1), and used pip on the command line to install as per instructions on the bottom of their download page.
QuickStart Guide for Pyglet: If you’ve never used Pyglet I would advise looking through this, just to get a feel for how Pyglet works. They can probably explain the basics better than I can!
This tutorial is based heavily upon this tutorial, but I have tried to go about extracting the game into components and systems.
So, to the code. First thing’s first we need to do the generic “setup” of a Pyglet application.
#main.py #I shorten the imported name import pyglet as pyg #give our window a size window = pyg.window.Window(800,600) #give our window a name! window.set_caption("PyggieSpaceWars") if __name__ == '__main__': pyg.app.run() #tell it to run!
When this piece of code is run it will produce a blank window with the caption we set in the top window bar. Pyglet has the window exit stuff set up already so we can close the window.
Now, we want to be able to add some images, for that we’ll need to set up our resources.
#game/resources.py from pyglet.gl import * import pyglet as pyg def set_anchor(image, anchor=None): if anchor == None: image.anchor_x = image.width/2 image.anchor_y = image.height/2 else: image.anchor_x = anchor image.anchor_y = anchor pyg.resource.path = ["../data"] pyg.resource.reindex() #look for an image in the data folder, in images called ship.png player_image = pyg.resource.image("images/ship.png") set_anchor(player_image) #openGL flags for scaling glEnable(GL_TEXTURE_2D) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
In this file we have defined our resource path to be in data, this means when we do the subsequent calls to pyg.resource.image(“someimage.png”) the “path” we set will be the origin point for these calls. So bear that in mind when setting and organising your image files.
In pyglet image files have an “anchor” point, around which any rotations are performed. Primarily we’ll probably want this to be around the centre of the image. So we define that clause, otherwise we can give the image a specific anchor location. If you don’t set the anchor point it is defaulted to the bottom left corner of the image.
Using some OpenGL flags we tell pyglet how we want it to scale out images. (I don’t completely understand what these flags do, I can guess, but the end result is what I want!) I found the solution here and so because everything in the game will be scaled with no blurring this is the method I’ve chosen!
Now, for our first components!
#game/components.py from ecs import Component class Transform(Component): """ The positional and rotational data of an entity""" def __init__(self, x=0, y=0, rotation=0): self.x = x self.y = y self.rotation = rotation class Renderable(Component): """The visual representation of an entity. """ def __init__(self, sprite): self.sprite = sprite
These two components are fairly self explanatory, Transform is the “where” and Renderable is the “what” (visually).
Now onto our first system, this is going to be the RenderingSystem to draw all our sprites onto the screen.
#game/systems.py from ecs import System class RenderingSystem(System): def __init__(self, world): System.__init__(self) self.world = world #we'll talk about this soon. def update(self, dt): """ this method must be overwritten in each system""" #this is in effect what we would put into the on_draw event in pyglet. self.world.window.clear() self.world.batch.draw()
When creating a new system we must be sure to call the super class’ constructor in it’s own constructor, making sure to explicitly pass self. Every System needs to have an update method which will be given the delta time(or time since last update/tick) this can be then used in calculations to keep the game frames per second independent. All our Renderable components have a sprite and these will be passed the main batch – then upon the batch.draw() call here it will draw the sprites. And prior to that we will need to clear the screen using window.clear()
I create a class called World in which I store things that there are only one of, it’s my means to pass things like the reference to the main batch/ game window etc. The Artemis Entity System also has a class like World (called EntityWorld) and it’s how the different managers can talk to each other, that is also how our World class will be used in future.
#main.py import pyglet as pyg from game.systems import RenderingSystem class World(): window = pyg.window.Window(800,600) batch = pyg.graphics.Batch() em = EntityManager() sm = SystemManager(em) def __init__(self): self.window.set_caption("PyggieSpaceWars") world = World() #initialise this if __name__ == '__main__': #create a rendering system and add it to the systems manager. r_system = RenderingSystem(world) world.sm.add_system(r_system) #schedule updates for 60 fps of the systems manager #which in turn will update all registered systems. pyg.clock.schedule_interval(world.sm.update, 1/60.0) pyg.app.run()
We have moved the Window initialisation to the World class along with the caption setting. In the main block we create an instance of the RenderingSystem and add the system to the system manager. The update method for each system is called in the order they are added to the manager. We then schedule the calling of the system managers update method at a rate of 60 fps.
When run, this will still produce just a black screen. So lets change that – finally!
#entity_factory.py from pyglet.sprite import Sprite from game.components import Transform, Renderable from game.resources import player_image def create_player(world, x, y): #we use the entity manager to create an entity. e = world.em.create_entity() player_ship = Sprite(player_image, x, y, batch = world.batch) player_ship.scale = 2 world.em.add_component(e, Renderable(player_ship)) world.em.add_component(e, Transform(x, y) return e
The Renderable component holds an instance of pyglet’s Sprite class. and we need to provide it with the image we loaded in resources.py we also scale it by two, and this is where the OpenGL flags will come in and prevent the image from being blurred when the image is scaled. The sprite also needs to be given the batch it will be drawn by, so using our handy link through world we give it that one.
#system.py class AnimationSystem(System): def __init__(self): System.__init__(self) def update(self, dt): for entity, renderable_component in self.entity_manager.pairs_for_type(Renderable): #we will naively assume all renderables have a transform component. transform_component = self.entity_manager.component_for_entity(entity, Transform) renderable_component.sprite.x = transform_component.x renderable_component.sprite.y = transform_component.y
This is named AnimationSystem, however, I’m not sure I feel comfortable with that yet, but it will do for now. It’s purpose at the moment is to move the sprite to the location held by the Transform component.
Now for us to use the create_player and AnimationSystem we have to edit our main.py file again
#main.py if __name__ == '__main__': #add the animation system to the system manager first, so it will be updated first. world.sm.add_system(AnimationSystem()) #Rendering System being added to system manager r_system = RenderingSystem(world, world.batch) world.sm.add_system(r_system) #create our player entity in the center of the screen entity_factory.create_player(world, world.window.width/2, world.window.height/2) pyg.clock.schedule_interval(world.sm.update, 1/60.0) # update at a rate of 60 fps pyg.app.run()
When running this program now, you should now have this:
That’s the end for this part!