Complete Roguelike Tutorial, using python3+pysdl2, part 0 code

From RogueBasin
(Difference between revisions)
Jump to: navigation, search
m (time)
(time)
Line 7: Line 7:
 
== time ==
 
== time ==
 
util/time.py
 
util/time.py
 +
download it at this tutorial's github
 +
 +
== manager
 +
./manager.py
 
<div style="background-color: #EEEEEE; border-style: dotted"><syntaxhighlight lang="python">
 
<div style="background-color: #EEEEEE; border-style: dotted"><syntaxhighlight lang="python">
"""Modified, pure Python implementation of PyGameSDL2's pygame_time."""
+
"""Basic scene manager."""
  
"""
+
import ctypes
    Original source code available at:
+
import os
        github.com/
+
            renpy/pygame_sdl2/blob/master/src/pygame_sdl2/pygame_time.pyx
+
  
    Altered source plain mark:
+
os.environ["PYSDL2_DLL_PATH"] = "C:\\lib\\SDL2-2.0.5-win32-x86"
    --------------------------------------------------------------------------
+
              _ _                    _                                  _
+
        /\   | | |                  | |                                | |
+
        /  \ | | |_ ___ _ __ ___  __| |    ___  ___  _  _ _ __ ___ ___| |
+
      / /\ \ | | __/ _ \ '__/ _ \/ _` |  / __|/ _ \| | | | '__/ __/ _ \ |
+
      / ____ \| | ||  __/ | |  __/ (_| |  \__ \ (_) | |_| | | | (_|  __/_|
+
    /_/    \_\_|\__\___|_|  \___|\__,_|  |___/\___/ \__,_|_|  \___\___(_)
+
  
    --------------------------------------------------------------------------
+
import sdl2
 +
import sdl2.ext
  
    PygameSDL2 Notice / zlib License:
+
from constants import (SCREEN_WIDTH, SCREEN_HEIGHT, TILE_SIZE, LIMIT_FPS,
    --------------------------------------------------------------------------
+
                      WINDOW_COLOR)
    # Original work:
+
from util.time import Clock
    #    Copyright 2014 Tom Rothamel <tom@rothamel.us>
+
    #    Copyright 2014 Patrick Dawson <pat@dw.is>
+
    # Modified work:
+
    #    Copyright 2017 Lucas Siqueira <lucas.morais.siqueira@gmail.com>
+
    #
+
    # This software is provided 'as-is', without any express or implied
+
    # warranty.  In no event will the authors be held liable for any damages
+
    # arising from the use of this software.
+
    #
+
    # Permission is granted to anyone to use this software for any purpose,
+
    # including commercial applications, and to alter it and redistribute it
+
    # freely, subject to the following restrictions:
+
    #
+
    # 1. The origin of this software must not be misrepresented; you must not
+
    #    claim that you wrote the original software. If you use this software
+
    #    in a product, an acknowledgment in the product documentation would be
+
    #    appreciated but is not required.
+
    # 2. Altered source versions must be plainly marked as such, and must not
+
    #    be misrepresented as being the original software.
+
    # 3. This notice may not be removed or altered from any source
+
    #    distribution.
+
    --------------------------------------------------------------------------
+
"""
+
  
  
import math
+
__all__ = ("Manager", "KeyboardStateController", "SceneBase", "Resources")
from sdl2 import SDL_Delay, SDL_GetTicks
+
  
  
__all__ = ('Clock', 'wait', 'get_time', 'get_delta')
+
Resources = sdl2.ext.Resources(
 +
    os.path.join(os.path.dirname(__file__), "resources"))
  
  
def get_delta(t0):
+
class Manager(object):
     """Get the time elapsed since the passed time."""
+
     """Manage scenes and the main game loop.
    return get_time - t0
+
  
 +
    At each loop the events are passed down to the active scene and it's
 +
    update method is called.
 +
    """
  
def get_time():
+
    def __init__(
    """Get the current time from SDL clock."""
+
        self, width=None, height=None, cols=None, rows=None, tile_size=None,
    return SDL_GetTicks()
+
        limit_fps=None, window_color=None
 +
    ):
 +
        """Initialization.
  
 +
        Args:
 +
            width (int): the width of the screen in pixels. Defaults to
 +
                constants.SCREEN_WIDTH
 +
            height (int): the height of the screen in pixels. Defaults to
 +
                constants.SCREEN_HEIGHT
 +
            tile_size (int): size of a (square) tile's side in pixels.
 +
                Defaults to constants.TILE_SIZE
 +
            limit_fps (int): maximum frames per second that should be drawn.
 +
                Defaults to constants.LIMIT_FPS
 +
            window_color (4-tuple): the window's background color, as a tuple
 +
                of 4 integers representing Red, Greehn, Blue and Alpha values
 +
                (0-255). Defaults to constants.WINDOW_COLOR
  
def wait(milliseconds):
+
        Usage:
    """Pause the program for an amount of time.
+
            m = Manager() # start with default parameters
 +
            m.set_scene(SceneBase)  # set a scene. This is a blank base scene
 +
            m.execute()  # call the main loop
 +
        """
 +
        # Set the default arguments
 +
        self.width = width or SCREEN_WIDTH
 +
        self.height = height or SCREEN_HEIGHT
 +
        self.tile_size = tile_size or TILE_SIZE
 +
        self.limit_fps = limit_fps or LIMIT_FPS
 +
        self.window_color = window_color or WINDOW_COLOR
  
    Will pause for a given number of milliseconds. This function sleeps the
+
        # Number of tile_size-sized drawable columns and rows on screen
    process to share the processor with other programs. A program that waits
+
        self.cols = self.width // self.tile_size
    for even a few milliseconds will consume very little processor time.
+
        self.rows = self.height // self.tile_size
  
    Usage:
+
        # Initialize with no scene
         wait(milliseconds)
+
         self.scene = None
  
    Returns:
+
        # Initialize the video system - this implicitly initializes some
         int (the actual number of milliseconds used)
+
         # necessary parts within the SDL2 DLL used by the video module.
    """
+
        #
    start = SDL_GetTicks()
+
        # You SHOULD call this before using any video related methods or
    SDL_Delay(int(milliseconds))
+
        # classes.
    return SDL_GetTicks() - start
+
        sdl2.ext.init()
  
 +
        # Create a new window (like your browser window or editor window,
 +
        # etc.) and give it a meaningful title and size. We definitely need
 +
        # this, if we want to present something to the user.
 +
        self.window = sdl2.ext.Window(
 +
            "Tiles", size=(self.width, self.height),
 +
            flags=sdl2.SDL_WINDOW_BORDERLESS)
  
class Clock:
+
        # Create a renderer that supports hardware-accelerated sprites.
    """Clock is used track and control the framerate of a game.
+
        self.renderer = sdl2.ext.Renderer(self.window)
  
    The clock can be used to limit the framerate of a game, as well as track
+
        # Create a sprite factory that allows us to create visible 2D elements
    the time used per frame.
+
        # easily.
 +
        self.factory = sdl2.ext.SpriteFactory(
 +
            sdl2.ext.TEXTURE, renderer=self.renderer)
  
     Usage:
+
        # Creates a simple rendering system for the Window. The
         clock = time.Clock()
+
        # SpriteRenderSystem can draw Sprite objects on the window.
     """
+
        self.spriterenderer = self.factory.create_sprite_render_system(
 +
            self.window)
 +
 
 +
        # By default, every Window is hidden, not shown on the screen right
 +
        # after creation. Thus we need to tell it to be shown now.
 +
        self.window.show()
 +
 
 +
        # Enforce window raising just to be sure.
 +
        sdl2.SDL_RaiseWindow(self.window.window)
 +
 
 +
        # Initialize the keyboard state controller.
 +
        # PySDL2/SDL2 shouldn't need this but the basic procedure for getting
 +
        # key mods and locks is not working for me atm.
 +
        # So I've implemented my own controller.
 +
        self.kb_state = KeyboardStateController()
 +
 
 +
        # Initialize a mouse starting position. From here on the manager will
 +
        # be able to work on distances from previous positions.
 +
        self._get_mouse_state()
 +
 
 +
        # Initialize a clock utility to help us control the framerate
 +
        self.clock = Clock()
 +
 
 +
        # Make the Manager alive. This is used on the main loop.
 +
        self.alive = True
 +
 
 +
     def _get_mouse_state(self):
 +
         """Get the mouse state.
 +
 
 +
        This is only required during initialization. Later on the mouse
 +
        position will be passed through events.
 +
        """
 +
        # This is an example of what PySDL2, below the hood, does for us.
 +
        # Here we create a ctypes int (i.e. a C type int)
 +
        x = ctypes.c_int(0)
 +
        y = ctypes.c_int(0)
 +
        # And pass it by reference to the SDL C function (i.e. pointers)
 +
        sdl2.mouse.SDL_GetMouseState(ctypes.byref(x), ctypes.byref(y))
 +
        # The variables were modified by SDL, but are still of C type
 +
        # So we need to get their values as python integers
 +
        self._mouse_x = x.value
 +
        self._mouse_y = y.value
 +
        # Now we hope we're never going to deal with this kind of stuff again
 +
        return self._mouse_x, self._mouse_y
 +
 
 +
    def run(self):
 +
        """Main loop handling events and updates."""
 +
        while self.alive:
 +
            self.clock.tick(self.limit_fps)
 +
            self.on_event()
 +
            self.on_update()
 +
        return sdl2.ext.quit()
 +
 
 +
    def on_event(self):
 +
        """Handle the events and pass them to the active scene."""
 +
        scene = self.scene
 +
 
 +
        if scene is None:
 +
            return
 +
        for event in sdl2.ext.get_events():
 +
 
 +
            # Exit events
 +
            if event.type == sdl2.SDL_QUIT:
 +
                self.alive = False
 +
                return
 +
 
 +
            # Redraw in case the focus was lost and now regained
 +
            if event.type == sdl2.SDL_WINDOWEVENT_FOCUS_GAINED:
 +
                self.on_update()
 +
                continue
 +
 
 +
            # on_mouse_motion, on_mouse_drag
 +
            if event.type == sdl2.SDL_MOUSEMOTION:
 +
                x = event.motion.x
 +
                y = event.motion.y
 +
                buttons = event.motion.state
 +
                self._mouse_x = x
 +
                self._mouse_y = y
 +
                dx = x - self._mouse_x
 +
                dy = y - self._mouse_y
 +
                if buttons & sdl2.SDL_BUTTON_LMASK:
 +
                    scene.on_mouse_drag(event, x, y, dx, dy, "LEFT")
 +
                elif buttons & sdl2.SDL_BUTTON_MMASK:
 +
                    scene.on_mouse_drag(event, x, y, dx, dy, "MIDDLE")
 +
                elif buttons & sdl2.SDL_BUTTON_RMASK:
 +
                    scene.on_mouse_drag(event, x, y, dx, dy, "RIGHT")
 +
                else:
 +
                    scene.on_mouse_motion(event, x, y, dx, dy)
 +
                continue
 +
            # on_mouse_press
 +
            elif event.type == sdl2.SDL_MOUSEBUTTONDOWN:
 +
                x = event.button.x
 +
                y = event.button.y
 +
 
 +
                button_n = event.button.button
 +
                if button_n == sdl2.SDL_BUTTON_LEFT:
 +
                    button = "LEFT"
 +
                elif button_n == sdl2.SDL_BUTTON_RIGHT:
 +
                    button = "RIGHT"
 +
                elif button_n == sdl2.SDL_BUTTON_MIDDLE:
 +
                    button = "MIDDLE"
 +
 
 +
                double = bool(event.button.clicks - 1)
 +
 
 +
                scene.on_mouse_press(event, x, y, button, double)
 +
                continue
 +
            # on_mouse_scroll (wheel)
 +
            elif event.type == sdl2.SDL_MOUSEWHEEL:
 +
                offset_x = event.wheel.x
 +
                offset_y = event.wheel.y
 +
                scene.on_mouse_scroll(event, offset_x, offset_y)
 +
                continue
 +
 
 +
            # for keyboard input, set the key symbol and keyboard modifiers
 +
            mod = self.kb_state.process(event)
 +
            sym = event.key.keysym.sym
 +
 
 +
            # on_key_release
 +
            if event.type == sdl2.SDL_KEYUP:
 +
                scene.on_key_release(event, sym, mod)
 +
            # on_key_press
 +
            elif event.type == sdl2.SDL_KEYDOWN:
 +
                scene.on_key_press(event, sym, mod)
 +
 
 +
     def on_update(self):
 +
        """Update the active scene."""
 +
        scene = self.scene
 +
        if self.alive:
 +
            # clear the window with its color
 +
            self.renderer.clear(self.window_color)
 +
            if scene:
 +
                # call the active scene's on_update
 +
                scene.on_update()
 +
            # present what we have to the screen
 +
            self.present()
 +
 
 +
    def present(self):
 +
        """Flip the GPU buffer."""
 +
        sdl2.render.SDL_RenderPresent(self.spriterenderer.sdlrenderer)
 +
 
 +
    def set_scene(self, scene=None, **kwargs):
 +
        """Set the scene.
 +
 
 +
        Args:
 +
            scene (SceneBase): the scene to be initialized
 +
            kwargs: the arguments that should be passed to the scene
 +
 
 +
        """
 +
        self.scene = scene(manager=self, **kwargs)
 +
 
 +
 
 +
class KeyboardStateController:
 +
    """A class that keeps track of keyboard modifiers and locks."""
  
 
     def __init__(self):
 
     def __init__(self):
 
         """Initialization."""
 
         """Initialization."""
         self.last = SDL_GetTicks()
+
         self._shift = False
         self.last_frames = []
+
         self._ctrl = False
         self.frametime = 0
+
         self._alt = False
         self.raw_frametime = 0
+
         self.caps = False
 +
        self.num = False
 +
        self.scroll = False
  
     def tick(self, framerate=0):
+
     def contains(self, *args):
         """Update the Clock.
+
         """..."""
 +
        d = {arg: True for arg in args}
 +
        return self.combine(**d)
  
         This method should be called once per frame. It will compute how many
+
    @property
         milliseconds have passed since the previous call.
+
    def alt(self):
 +
         """..."""
 +
         return self.combine(ctrl=True)
  
        If you pass the optional framerate argument the function will delay
+
    @property
        to keep the game running slower than the given ticks per second. This
+
    def ctrl(self):
         can be used to help limit the runtime speed of a game. By calling
+
         """..."""
         clock.tick(40) once per frame, the program will never run at more
+
         return self.combine(ctrl=True)
        than 40 frames per second.
+
  
         Usage:
+
    @property
             tick(framerate=0)
+
    def shift(self):
 +
         """..."""
 +
        return self.combine(shift=True)
 +
 
 +
    def combine(self, alt=False, ctrl=False, shift=False):
 +
        """..."""
 +
        return all(
 +
             (self._alt == alt,
 +
            self._ctrl == ctrl,
 +
            self._shift == shift)
 +
        )
 +
 
 +
    def process(self, event):
 +
        """Process the current event and update the keyboard state."""
 +
        down = True if event.type == sdl2.SDL_KEYDOWN else False
 +
        self._process_mods(event.key.keysym.sym, down)
 +
        if not down:
 +
            self._process_locks(event.key.keysym.sym)
 +
        return self
 +
 
 +
    def _process_locks(self, key):
 +
        """Process the locks."""
 +
        for lock, sym in (
 +
            ("caps", sdl2.SDLK_CAPSLOCK),
 +
            ("num", sdl2.SDLK_NUMLOCKCLEAR),
 +
            ("scroll", sdl2.SDLK_SCROLLLOCK)
 +
        ):
 +
            if key == sym:
 +
                _prev_lock = getattr(self, lock)
 +
                setattr(self, lock, not _prev_lock)
 +
 
 +
    def _process_mods(self, key, down):
 +
        """Process the modifiers."""
 +
        for mod, syms in (
 +
            ("_ctrl", (sdl2.SDLK_LCTRL, sdl2.SDLK_RCTRL)),
 +
            ("_shift", (sdl2.SDLK_LSHIFT, sdl2.SDLK_RSHIFT)),
 +
            ("_alt", (sdl2.SDLK_LALT, sdl2.SDLK_RALT))
 +
        ):
 +
            if key in syms:
 +
                setattr(self, mod, down)
 +
 
 +
    def __getstate__(self):
 +
        """Prevent pickling."""
 +
        return None
 +
 
 +
    def __repr__(self):
 +
        """Representation of keyboard states."""
 +
        return (
 +
            "alt: %r, ctrl: %r, shift: %r, caps: %r, num: %r, scroll %r" %
 +
            (self.alt, self.ctrl, self.shift, self.caps, self.num,
 +
            self.scroll))
 +
 
 +
 
 +
class SceneBase(object):
 +
    """Basic scene of the game.
 +
 
 +
    New Scenes should be subclasses of SceneBase.
 +
    """
 +
 
 +
    def __new__(cls, manager, **kwargs):
 +
        """Create a new instance of a scene.
 +
 
 +
        A reference to the manager is stored before returning the instance.
 +
        This is made preventively because many properties are related to the
 +
        manager.
 +
 
 +
        Args:
 +
            manager (Manager): the running instance of the Manager
 +
        """
 +
        scene = super().__new__(cls)
 +
        scene.manager = manager
 +
        return scene
 +
 
 +
    def __init__(self, **kwargs):
 +
        """Initialization."""
 +
        pass
 +
 
 +
    # properties
 +
    @property
 +
    def height(self):
 +
        """Main window height.
  
 
         Returns:
 
         Returns:
             float (milliseconds)
+
             Manager.height
 
         """
 
         """
         now = SDL_GetTicks()
+
         return self.manager.height
        self.raw_frametime = now - self.last
+
        while len(self.last_frames) > 9:
+
            self.last_frames.pop(0)
+
        if framerate == 0:
+
            self.last = now
+
            self.last_frames.append(self.raw_frametime)
+
            return self.raw_frametime
+
        frame_duration = 1.0 / framerate * 1000
+
        if self.raw_frametime < frame_duration:
+
            wait(frame_duration - self.raw_frametime)
+
        now = SDL_GetTicks()
+
        self.frametime = now - self.last
+
        self.last = now
+
        self.last_frames.append(self.frametime)
+
        return self.frametime
+
  
     def get_time(self):
+
    @property
         """Time used in the previous tick.
+
     def width(self):
 +
         """Main window width.
  
         Returns the parameter passed to the last call to Clock.tick().
+
         Returns:
 +
            Manager.height
 +
        """
 +
        return self.manager.width
  
        Usage:
+
    @property
            clock.get_time()
+
    def factory(self):
 +
        """Reference to sdl2.ext.SpriteFactory instance.
  
 
         Returns:
 
         Returns:
             int (milliseconds)
+
             Manager.factory
 
         """
 
         """
         return self.frametime
+
         return self.manager.factory
  
     def get_rawtime(self):
+
    @property
         """Actual time used in the previous tick.
+
     def kb_state(self):
 +
         """Reference to KeyboardStateController instance.
  
         Similar to Clock.get_time(), but this does not include any time used
+
         Returns:
         while clock.tick() was delaying to limit the framerate.
+
            Manager.kb_state
 +
        """
 +
         return self.manager.kb_state
  
        Usage:
+
    @property
            clock.get_rawtime()
+
    def renderer(self):
 +
        """Reference to sdl2.ext.Renderer instance.
  
 
         Returns:
 
         Returns:
             int (milliseconds)
+
             Manager.renderer
 +
 
 
         """
 
         """
         return self.raw_frametime
+
         return self.manager.renderer
  
     def get_fps(self):
+
    @property
         """Compute the clock framerate.
+
     def sdlrenderer(self):
 +
         """Reference to sdl2.SDL_Renderer instance.
  
         Compute your game’s framerate (in frames per second). It is computed
+
         Returns:
         by averaging the last ten calls to Clock.tick().
+
            Manager.renderer.sdlrenderer
 +
         """
 +
        return self.manager.renderer.sdlrenderer
  
        Usage:
+
    @property
            get_fps()
+
    def spriterenderer(self):
 +
        """Reference to sdl2.ext.TextureSpriteRenderSystem instance.
  
 
         Returns:
 
         Returns:
             float
+
             Manager.spriterenderer
 
         """
 
         """
         total_time = sum(self.last_frames)
+
         return self.manager.spriterenderer
         average_time = total_time / 1000.0 / len(self.last_frames)
+
 
         average_fps = 1.0 / average_time
+
    # other methods
         return 0 if math.isnan(average_fps) else average_fps
+
    def quit(self):
 +
        """Stop the manager main loop."""
 +
        self.manager.alive = False
 +
 
 +
    # event methods
 +
    def on_key_press(self, event, sym, mod):
 +
        """Called on keyboard input, when a key is **held down**.
 +
 
 +
        Args:
 +
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
 +
                Unless specifically needed, sym and mod should be used
 +
                instead.
 +
            sym (int): Integer representing code of the key pressed. For
 +
                printable keys ``chr(key)`` should return the corresponding
 +
                character.
 +
            mod (KeyboardStateController): the keyboard state for modifiers
 +
                and locks. See :class:KeyboardStateController
 +
         """
 +
        pass
 +
 
 +
    def on_key_release(self, event, sym, mod):
 +
        """Called on keyboard input, when a key is **released**.
 +
 
 +
        By default if the Escape key is pressed the manager quits.
 +
        If that behaviour is desired you can call ``super().on_key_release(
 +
        event, sym, mod)`` on a child class.
 +
 
 +
        Args:
 +
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
 +
                The other arguments should be used for a higher level
 +
                interaction, unless specifically needed.
 +
            sym (int): Integer representing code of the key pressed. For
 +
                printable keys ``chr(key)`` should return the corresponding
 +
                character.
 +
            mod (KeyboardStateController): the keyboard state for modifiers
 +
                and locks. See :class:KeyboardStateController
 +
        """
 +
        if sym == sdl2.SDLK_ESCAPE:
 +
            self.quit()
 +
 
 +
    def on_mouse_drag(self, event, x, y, dx, dy, button):
 +
        """Called when mouse buttons are pressed and the mouse is dragged.
 +
 
 +
        Args:
 +
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
 +
                The other arguments should be used for a higher level
 +
                interaction, unless specifically needed.
 +
            x (int): horizontal coordinate, relative to window.
 +
            y (int): vertical coordinate, relative to window.
 +
            dx (int): relative motion in the horizontal direction
 +
            dy (int): relative motion in the vertical direction
 +
            button (str, "RIGHT"|"MIDDLE"|"LEFT"): string representing the
 +
                button pressed.
 +
         """
 +
        pass
 +
 
 +
    def on_mouse_motion(self, event, x, y, dx, dy):
 +
        """Called when the mouse is moved.
 +
 
 +
         Args:
 +
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
 +
                The other arguments should be used for a higher level
 +
                interaction, unless specifically needed.
 +
            x (int): horizontal coordinate, relative to window.
 +
            y (int): vertical coordinate, relative to window.
 +
            dx (int): relative motion in the horizontal direction
 +
            dy (int): relative motion in the vertical direction
 +
        """
 +
        pass
 +
 
 +
    def on_mouse_press(self, event, x, y, button, double):
 +
        """Called when mouse buttons are pressed.
 +
 
 +
        Args:
 +
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
 +
                The other arguments should be used for a higher level
 +
                interaction, unless specifically needed.
 +
            x (int): horizontal coordinate, relative to window.
 +
            y (int): vertical coordinate, relative to window.
 +
            button (str, "RIGHT"|"MIDDLE"|"LEFT"): string representing the
 +
                button pressed.
 +
            double (bool, True|False): boolean indicating if the click was a
 +
                double click.
 +
        """
 +
        pass
 +
 
 +
    def on_mouse_scroll(self, event, offset_x, offset_y):
 +
        """Called when the mouse wheel is scrolled.
 +
 
 +
        Args:
 +
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
 +
                The other arguments should be used for a higher level
 +
                interaction, unless specifically needed.
 +
            offset_x (int): the amount scrolled horizontally, positive to the
 +
                right and negative to the left.
 +
            offset_y (int): the amount scrolled vertically, positive away
 +
                from the user and negative toward the user.
 +
        """
 +
        pass
 +
 
 +
    def on_update(self):
 +
        """Graphical logic."""
 +
        pass
 +
 
 +
 
 +
if __name__ == '__main__':
 +
    # example, with a borderless yet ugly green window
 +
    m = Manager(window_color=(0, 255, 0, 255))
 +
    m.set_scene(scene=SceneBase)
 +
    m.run()
  
 
</syntaxhighlight></div>
 
</syntaxhighlight></div>
 
[[Category:Developing]]
 
[[Category:Developing]]

Revision as of 05:28, 4 January 2017

This is part of a series of tutorials; the main page can be found here.


time

util/time.py download it at this tutorial's github

== manager ./manager.py

"""Basic scene manager."""
 
import ctypes
import os
 
os.environ["PYSDL2_DLL_PATH"] = "C:\\lib\\SDL2-2.0.5-win32-x86"
 
import sdl2
import sdl2.ext
 
from constants import (SCREEN_WIDTH, SCREEN_HEIGHT, TILE_SIZE, LIMIT_FPS,
                       WINDOW_COLOR)
from util.time import Clock
 
 
__all__ = ("Manager", "KeyboardStateController", "SceneBase", "Resources")
 
 
Resources = sdl2.ext.Resources(
    os.path.join(os.path.dirname(__file__), "resources"))
 
 
class Manager(object):
    """Manage scenes and the main game loop.
 
    At each loop the events are passed down to the active scene and it's
    update method is called.
    """
 
    def __init__(
        self, width=None, height=None, cols=None, rows=None, tile_size=None,
        limit_fps=None, window_color=None
    ):
        """Initialization.
 
        Args:
            width (int): the width of the screen in pixels. Defaults to
                constants.SCREEN_WIDTH
            height (int): the height of the screen in pixels. Defaults to
                constants.SCREEN_HEIGHT
            tile_size (int): size of a (square) tile's side in pixels.
                Defaults to constants.TILE_SIZE
            limit_fps (int): maximum frames per second that should be drawn.
                Defaults to constants.LIMIT_FPS
            window_color (4-tuple): the window's background color, as a tuple
                of 4 integers representing Red, Greehn, Blue and Alpha values
                (0-255). Defaults to constants.WINDOW_COLOR
 
        Usage:
            m = Manager()  # start with default parameters
            m.set_scene(SceneBase)  # set a scene. This is a blank base scene
            m.execute()  # call the main loop
        """
        # Set the default arguments
        self.width = width or SCREEN_WIDTH
        self.height = height or SCREEN_HEIGHT
        self.tile_size = tile_size or TILE_SIZE
        self.limit_fps = limit_fps or LIMIT_FPS
        self.window_color = window_color or WINDOW_COLOR
 
        # Number of tile_size-sized drawable columns and rows on screen
        self.cols = self.width // self.tile_size
        self.rows = self.height // self.tile_size
 
        # Initialize with no scene
        self.scene = None
 
        # Initialize the video system - this implicitly initializes some
        # necessary parts within the SDL2 DLL used by the video module.
        #
        # You SHOULD call this before using any video related methods or
        # classes.
        sdl2.ext.init()
 
        # Create a new window (like your browser window or editor window,
        # etc.) and give it a meaningful title and size. We definitely need
        # this, if we want to present something to the user.
        self.window = sdl2.ext.Window(
            "Tiles", size=(self.width, self.height),
            flags=sdl2.SDL_WINDOW_BORDERLESS)
 
        # Create a renderer that supports hardware-accelerated sprites.
        self.renderer = sdl2.ext.Renderer(self.window)
 
        # Create a sprite factory that allows us to create visible 2D elements
        # easily.
        self.factory = sdl2.ext.SpriteFactory(
            sdl2.ext.TEXTURE, renderer=self.renderer)
 
        # Creates a simple rendering system for the Window. The
        # SpriteRenderSystem can draw Sprite objects on the window.
        self.spriterenderer = self.factory.create_sprite_render_system(
            self.window)
 
        # By default, every Window is hidden, not shown on the screen right
        # after creation. Thus we need to tell it to be shown now.
        self.window.show()
 
        # Enforce window raising just to be sure.
        sdl2.SDL_RaiseWindow(self.window.window)
 
        # Initialize the keyboard state controller.
        # PySDL2/SDL2 shouldn't need this but the basic procedure for getting
        # key mods and locks is not working for me atm.
        # So I've implemented my own controller.
        self.kb_state = KeyboardStateController()
 
        # Initialize a mouse starting position. From here on the manager will
        # be able to work on distances from previous positions.
        self._get_mouse_state()
 
        # Initialize a clock utility to help us control the framerate
        self.clock = Clock()
 
        # Make the Manager alive. This is used on the main loop.
        self.alive = True
 
    def _get_mouse_state(self):
        """Get the mouse state.
 
        This is only required during initialization. Later on the mouse
        position will be passed through events.
        """
        # This is an example of what PySDL2, below the hood, does for us.
        # Here we create a ctypes int (i.e. a C type int)
        x = ctypes.c_int(0)
        y = ctypes.c_int(0)
        # And pass it by reference to the SDL C function (i.e. pointers)
        sdl2.mouse.SDL_GetMouseState(ctypes.byref(x), ctypes.byref(y))
        # The variables were modified by SDL, but are still of C type
        # So we need to get their values as python integers
        self._mouse_x = x.value
        self._mouse_y = y.value
        # Now we hope we're never going to deal with this kind of stuff again
        return self._mouse_x, self._mouse_y
 
    def run(self):
        """Main loop handling events and updates."""
        while self.alive:
            self.clock.tick(self.limit_fps)
            self.on_event()
            self.on_update()
        return sdl2.ext.quit()
 
    def on_event(self):
        """Handle the events and pass them to the active scene."""
        scene = self.scene
 
        if scene is None:
            return
        for event in sdl2.ext.get_events():
 
            # Exit events
            if event.type == sdl2.SDL_QUIT:
                self.alive = False
                return
 
            # Redraw in case the focus was lost and now regained
            if event.type == sdl2.SDL_WINDOWEVENT_FOCUS_GAINED:
                self.on_update()
                continue
 
            # on_mouse_motion, on_mouse_drag
            if event.type == sdl2.SDL_MOUSEMOTION:
                x = event.motion.x
                y = event.motion.y
                buttons = event.motion.state
                self._mouse_x = x
                self._mouse_y = y
                dx = x - self._mouse_x
                dy = y - self._mouse_y
                if buttons & sdl2.SDL_BUTTON_LMASK:
                    scene.on_mouse_drag(event, x, y, dx, dy, "LEFT")
                elif buttons & sdl2.SDL_BUTTON_MMASK:
                    scene.on_mouse_drag(event, x, y, dx, dy, "MIDDLE")
                elif buttons & sdl2.SDL_BUTTON_RMASK:
                    scene.on_mouse_drag(event, x, y, dx, dy, "RIGHT")
                else:
                    scene.on_mouse_motion(event, x, y, dx, dy)
                continue
            # on_mouse_press
            elif event.type == sdl2.SDL_MOUSEBUTTONDOWN:
                x = event.button.x
                y = event.button.y
 
                button_n = event.button.button
                if button_n == sdl2.SDL_BUTTON_LEFT:
                    button = "LEFT"
                elif button_n == sdl2.SDL_BUTTON_RIGHT:
                    button = "RIGHT"
                elif button_n == sdl2.SDL_BUTTON_MIDDLE:
                    button = "MIDDLE"
 
                double = bool(event.button.clicks - 1)
 
                scene.on_mouse_press(event, x, y, button, double)
                continue
            # on_mouse_scroll (wheel)
            elif event.type == sdl2.SDL_MOUSEWHEEL:
                offset_x = event.wheel.x
                offset_y = event.wheel.y
                scene.on_mouse_scroll(event, offset_x, offset_y)
                continue
 
            # for keyboard input, set the key symbol and keyboard modifiers
            mod = self.kb_state.process(event)
            sym = event.key.keysym.sym
 
            # on_key_release
            if event.type == sdl2.SDL_KEYUP:
                scene.on_key_release(event, sym, mod)
            # on_key_press
            elif event.type == sdl2.SDL_KEYDOWN:
                scene.on_key_press(event, sym, mod)
 
    def on_update(self):
        """Update the active scene."""
        scene = self.scene
        if self.alive:
            # clear the window with its color
            self.renderer.clear(self.window_color)
            if scene:
                # call the active scene's on_update
                scene.on_update()
            # present what we have to the screen
            self.present()
 
    def present(self):
        """Flip the GPU buffer."""
        sdl2.render.SDL_RenderPresent(self.spriterenderer.sdlrenderer)
 
    def set_scene(self, scene=None, **kwargs):
        """Set the scene.
 
        Args:
            scene (SceneBase): the scene to be initialized
            kwargs: the arguments that should be passed to the scene
 
        """
        self.scene = scene(manager=self, **kwargs)
 
 
class KeyboardStateController:
    """A class that keeps track of keyboard modifiers and locks."""
 
    def __init__(self):
        """Initialization."""
        self._shift = False
        self._ctrl = False
        self._alt = False
        self.caps = False
        self.num = False
        self.scroll = False
 
    def contains(self, *args):
        """..."""
        d = {arg: True for arg in args}
        return self.combine(**d)
 
    @property
    def alt(self):
        """..."""
        return self.combine(ctrl=True)
 
    @property
    def ctrl(self):
        """..."""
        return self.combine(ctrl=True)
 
    @property
    def shift(self):
        """..."""
        return self.combine(shift=True)
 
    def combine(self, alt=False, ctrl=False, shift=False):
        """..."""
        return all(
            (self._alt == alt,
             self._ctrl == ctrl,
             self._shift == shift)
        )
 
    def process(self, event):
        """Process the current event and update the keyboard state."""
        down = True if event.type == sdl2.SDL_KEYDOWN else False
        self._process_mods(event.key.keysym.sym, down)
        if not down:
            self._process_locks(event.key.keysym.sym)
        return self
 
    def _process_locks(self, key):
        """Process the locks."""
        for lock, sym in (
            ("caps", sdl2.SDLK_CAPSLOCK),
            ("num", sdl2.SDLK_NUMLOCKCLEAR),
            ("scroll", sdl2.SDLK_SCROLLLOCK)
        ):
            if key == sym:
                _prev_lock = getattr(self, lock)
                setattr(self, lock, not _prev_lock)
 
    def _process_mods(self, key, down):
        """Process the modifiers."""
        for mod, syms in (
            ("_ctrl", (sdl2.SDLK_LCTRL, sdl2.SDLK_RCTRL)),
            ("_shift", (sdl2.SDLK_LSHIFT, sdl2.SDLK_RSHIFT)),
            ("_alt", (sdl2.SDLK_LALT, sdl2.SDLK_RALT))
        ):
            if key in syms:
                setattr(self, mod, down)
 
    def __getstate__(self):
        """Prevent pickling."""
        return None
 
    def __repr__(self):
        """Representation of keyboard states."""
        return (
            "alt: %r, ctrl: %r, shift: %r, caps: %r, num: %r, scroll %r" %
            (self.alt, self.ctrl, self.shift, self.caps, self.num,
             self.scroll))
 
 
class SceneBase(object):
    """Basic scene of the game.
 
    New Scenes should be subclasses of SceneBase.
    """
 
    def __new__(cls, manager, **kwargs):
        """Create a new instance of a scene.
 
        A reference to the manager is stored before returning the instance.
        This is made preventively because many properties are related to the
        manager.
 
        Args:
            manager (Manager): the running instance of the Manager
        """
        scene = super().__new__(cls)
        scene.manager = manager
        return scene
 
    def __init__(self, **kwargs):
        """Initialization."""
        pass
 
    # properties
    @property
    def height(self):
        """Main window height.
 
        Returns:
            Manager.height
        """
        return self.manager.height
 
    @property
    def width(self):
        """Main window width.
 
        Returns:
            Manager.height
        """
        return self.manager.width
 
    @property
    def factory(self):
        """Reference to sdl2.ext.SpriteFactory instance.
 
        Returns:
            Manager.factory
        """
        return self.manager.factory
 
    @property
    def kb_state(self):
        """Reference to KeyboardStateController instance.
 
        Returns:
            Manager.kb_state
        """
        return self.manager.kb_state
 
    @property
    def renderer(self):
        """Reference to sdl2.ext.Renderer instance.
 
        Returns:
            Manager.renderer
 
        """
        return self.manager.renderer
 
    @property
    def sdlrenderer(self):
        """Reference to sdl2.SDL_Renderer instance.
 
        Returns:
            Manager.renderer.sdlrenderer
        """
        return self.manager.renderer.sdlrenderer
 
    @property
    def spriterenderer(self):
        """Reference to sdl2.ext.TextureSpriteRenderSystem instance.
 
        Returns:
            Manager.spriterenderer
        """
        return self.manager.spriterenderer
 
    # other methods
    def quit(self):
        """Stop the manager main loop."""
        self.manager.alive = False
 
    # event methods
    def on_key_press(self, event, sym, mod):
        """Called on keyboard input, when a key is **held down**.
 
        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                Unless specifically needed, sym and mod should be used
                instead.
            sym (int): Integer representing code of the key pressed. For
                printable keys ``chr(key)`` should return the corresponding
                character.
            mod (KeyboardStateController): the keyboard state for modifiers
                and locks. See :class:KeyboardStateController
        """
        pass
 
    def on_key_release(self, event, sym, mod):
        """Called on keyboard input, when a key is **released**.
 
        By default if the Escape key is pressed the manager quits.
        If that behaviour is desired you can call ``super().on_key_release(
        event, sym, mod)`` on a child class.
 
        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                The other arguments should be used for a higher level
                interaction, unless specifically needed.
            sym (int): Integer representing code of the key pressed. For
                printable keys ``chr(key)`` should return the corresponding
                character.
            mod (KeyboardStateController): the keyboard state for modifiers
                and locks. See :class:KeyboardStateController
        """
        if sym == sdl2.SDLK_ESCAPE:
            self.quit()
 
    def on_mouse_drag(self, event, x, y, dx, dy, button):
        """Called when mouse buttons are pressed and the mouse is dragged.
 
        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                The other arguments should be used for a higher level
                interaction, unless specifically needed.
            x (int): horizontal coordinate, relative to window.
            y (int): vertical coordinate, relative to window.
            dx (int): relative motion in the horizontal direction
            dy (int): relative motion in the vertical direction
            button (str, "RIGHT"|"MIDDLE"|"LEFT"): string representing the
                button pressed.
        """
        pass
 
    def on_mouse_motion(self, event, x, y, dx, dy):
        """Called when the mouse is moved.
 
        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                The other arguments should be used for a higher level
                interaction, unless specifically needed.
            x (int): horizontal coordinate, relative to window.
            y (int): vertical coordinate, relative to window.
            dx (int): relative motion in the horizontal direction
            dy (int): relative motion in the vertical direction
        """
        pass
 
    def on_mouse_press(self, event, x, y, button, double):
        """Called when mouse buttons are pressed.
 
        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                The other arguments should be used for a higher level
                interaction, unless specifically needed.
            x (int): horizontal coordinate, relative to window.
            y (int): vertical coordinate, relative to window.
            button (str, "RIGHT"|"MIDDLE"|"LEFT"): string representing the
                button pressed.
            double (bool, True|False): boolean indicating if the click was a
                double click.
        """
        pass
 
    def on_mouse_scroll(self, event, offset_x, offset_y):
        """Called when the mouse wheel is scrolled.
 
        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                The other arguments should be used for a higher level
                interaction, unless specifically needed.
            offset_x (int): the amount scrolled horizontally, positive to the
                right and negative to the left.
            offset_y (int): the amount scrolled vertically, positive away
                from the user and negative toward the user.
        """
        pass
 
    def on_update(self):
        """Graphical logic."""
        pass
 
 
if __name__ == '__main__':
    # example, with a borderless yet ugly green window
    m = Manager(window_color=(0, 255, 0, 255))
    m.set_scene(scene=SceneBase)
    m.run()
Personal tools