cxr

Math, code, people

Sen pro Kyne. Emotionally overavailable.

Yeah I mean https://cohost.org/sen-guide but I'm going through some things right now

posts from @cxr tagged #pygame

also:

I had never really worked with art assets at a very large scale, because backend is my usual thing. So when I found out about Linker's 8-bit Adventure Tiles it was too good to pass up. It's a really fun set with two palettes, one pico-8 style and the other nes style.

It was clear that I needed a consistent mechanism for chopping up and creating objects from each tile. So I created a library for managing sprites in the simplest way possible. For example, creating and managing a Player is way easy:

from linker import Player

player = Player(palette="nes")
player.change_state("walk")
player.turn_left()
player.update()

I put together every object in the set, from statues and stairs to accent blocks and treasure chests. It all fits neatly into a single dictionary which each class draws from to construct a particular object.

I really loved the process and I learned a lot from it, so I made the repo public to make use of the tileset more accessible to Python devs.

I was caught between making a tutorial game project and making videos, or just implementing the library in my own game, and I decided on the latter. Right now I'm thinking it'll be puzzle-based, with palette shifting as a mechanic to make full use of the tiles.



You can get the full details in the readme for cxr.state.state.

In summary, I realized while working with pygame that I needed a consistent and repeatable way to give stateful behavior to game objects. So I figured out a way to utilize a doubly-nested function as a decorator. So I can do something like this...

@player.add_state("falling")
def player_falling(event):
    self.gravity += 0.1
    # etc, behavior goes here

And that's the usual boilerplate pretty much entirely eliminated.

The decorator is a bit hard to grok, but take a look and then I'll explain it:

def add_state(self, key):

    def outer_wrapper(f):

        def inner_wrapper(event):
            f(event)

        if key in self._states:
            raise StateError(f"State add failed: {self.name}({self.key}) StateManager already has a state with key {key}")
        else:
            self._states[key] = inner_wrapper
            if not self._current:
                self._current = self._states[key]
                self._current_key = key

    return outer_wrapper

The important behavior is that if your decorator is a function that a) takes arguments and 1) returns a function, then that returned function is what is applied to the decorated function. So @player.add_state("falling") is calling the add_state function, which returns outer_wrapper, and that function is applied as the decorator which catalogues the state accordingly. This pattern utilizes something called a closure.

The decorator for the controller (the core function for default behavior and whatnot) is much simpler:

def controller(self, f):
    self._controller = f

And decorators really CAN be that simple. Its applied behaviors can be counterintuitive, but if you're finding yourself wrapping a lot of unrelated stuff in very similar-looking stuff, a decorator is probably what you need.