Homemmm​.s‑ol.nu

pleasant experiments with shaders

LÖVE, Lua, Photoshop + Games

Recently I’ve been building a 2d game engine for my semester project at CGL. Below, I’ll copy and paste two blog posts from my internal documentation blog:


After nailing down the basic narrative and gameplay idea in a lengthy group discussion on the first day, we each set goals for the next few days in order to kickstart the project.

As we decided on a pixel art art approach and point-and-click gameplay, as the programmer I decided to write a custom engine in LÖVE instead of using a bulky engine like Unity that we wouldn’t really profit from anyway and that would be less specific, and therefore less fitting, than what I could come up with. Following this, my goal as an engine programmer was then to reduce any effort needed to put content into the engine to a minimum. I remembered iteration speeds and how most things I did as a programmer last time I built a pixel art project in Unity really weren’t programming but replacing assets; adding states in an animation state machine that I could’ve easily coded by hand in a fraction of the time; and generally wrestling with the development environment, and I wanted to instead create a working environment that would make my job as the gameplay programmer and the job of the artist(s) a lot easier.

Because creating my own engine is not a minor task and obviously a very crucial one for the project, the decision to go ahead and not use a prebuilt Engine was to be thought out thoroughly, if I am to write my own engine it needs to work at least as good as a prebuilt one or it will harm the project. To test whether I could actually improve workflow and build something my team can profit from, without degrading the project work during the time I need to actually put it together, I set myself a three-day deadline to try and go as far as I can and see whether writing the engine myself seemed realistic. The other goal of this trial phase was to show my team members why the switch could be worthwhile for them also.

Therefore my main starting point was to build a feature that they would see as useful and that was central to what the engine was going to do later: rendering and scene management.

Specifically, I started writing something that loads a .PSD directly and animates the sublayers. I also added code that detects file changes and reloads them automatically. At the end of the second project day, I had this:

The code for this isn’t much, but it took a few iterations to make the file- reload-watching reuseable and work on Linux and Windows alike (and efficient). I used a 3rd party library for loading the photoshop files, but I had to patch it a lot. Here’s the main part for the animated-layer-loading:

artal = require "lib.artal.artal"

ALL_SHEETS = setmetatable {}, __mode: 'v'
RECHECK = 0.3

class PSDSheet
  new: (@filename, @frametime=.1) =>
    @time   = 0
    @frame  = 1

    @reload!

    if WATCHER
      WATCHER\register @filename, @

  reload: =>
    print "reloading #{@filename}..."


    @frames = {}
    target = @frames
    local group

    psd = artal.newPSD @filename
    for layer in *psd
      if layer.type == "open"
        if not group
          layer.image = love.graphics.newCanvas psd.width, psd.height
          love.graphics.setCanvas layer.image
          table.insert target, layer
          group = layer.name
      elseif layer.type == "close"
        if layer.name == group
          love.graphics.setCanvas!
          group = nil
      else
        if not group
          table.insert target, layer
        else
          love.graphics.draw layer.image, -layer.ox, -layer.oy if layer.image

  update: (dt) =>
    @time += dt

    @frame = 1 + (math.floor(@time/@frametime) % #@frames)

  draw: (x, y, rot) =>
    {:image, :ox, :oy} = @frames[@frame]
    love.graphics.draw image, x, y, rot, nil, nil, ox, oy if image

{
  :PSDSheet,
}

After being able to load simple animations from photoshop without even closing the game was working, I tried loading the scene İlke had created meanwhile:

Naturally, this failed at first. He was using clipping masks and blend modes, neither of which were implemented at the time, but after I made a few changes to hide the affected layers for the time being, it looked fine.

My overall engine goal was to be able to build something like 90% of the level right in photoshop - including animations, hit areas, player spawns etc. Basically I wanted to use Photoshop as a full level editor and only write gameplay scripts and engine code outside of it.

This meant that there would be very different types of information inside a level .psd that we would need to be accessible to the engine.

To load information and behaviours into the level structure that is loaded from the PSD I created a layer naming conventions; layers can specify a Directive afte their name.

The most important Directive (so far) is load: it loads a lua/moonscript mixin via it’s name and passes arguments to it . For example there is a ‘common’ (shared between different scenes/levels) module called subanim that treat’s a group inside a bigger, non-animated photoshop document as an animation (like what I did in the first post, but inside a complex scene). The subanim module has one parameter, the frame-duration (in seconds). To turn a group into a subanim, you have to append ‘load:subanim,0.2’ to it’s name for example.

Another Directive is tag, it stores the layer under a name so other scripts can access it specifically and consistently. Because this could also be done by a mixin, i am thinking about abolishing the Directive concept and only using mixins (so that the load could be removed also). You can see the system working in this clip:

making an animation out of the single rain layer

Moreover, I added a directory structure for mixins; to load a mixin called name for a scene called scene, it first looks in the scene specific directories:

  • game/scene/name.moon
  • game/scene.moon (this file can return multiple mixins)

if neither of these exist, it checks for common mixins of this name in the common directory and files:

  • games/common/name.moon
  • games/common.moon (this file can return multiple mixins)

This allows to share mixins between scenes (like click-area mixins maybe, or the subanim mentioned above) but still keep a clean directory structure for specific elements (like the dialogue of a certain scene, or the tram in the background of the scene we are working on currently).

Mixin code can modify the scene node / layer object and overwrite the default “draw” and “update” hooks/methods. This allows for nearly everything I can think of right now, but most mixins are still very short and concise. As examples, you can take a look at the code that animates the tram in the background or the subanim source:

game/first_encounter/tram.moon:

import wrapping_, Mixin from  require "util"

wrapping_ class SubAnim extends Mixin
  SPEED = 440
  new: (scene) =>
    super!

    @pos = 0

  update: (dt) =>
    @pos = (@pos + SPEED*dt) % (WIDTH*2)

  draw: (recursive_draw) =>
    love.graphics.draw @image, @pos/4 - @ox - 140, -@oy

game/common/subanim.moon:

import wrapping_, Mixin from  require "util"

wrapping_ class SubAnim extends Mixin
  new: (scene, @frametime=0.1) =>
    super!

    @time = 0
    @frame = 1

  update: (dt) =>
    @time += dt

    @frame = 1 + (math.floor(@time/@frametime) % #@)

  draw: (recursive_draw) =>
    recursive_draw {@[@frame]}

wrapping is a small helper that allows a moonscript class to wrap an existing lua table (= object, in this case the layer objects produced by the psd parsing phase) and Mixin is a class that handles mixin live-reloading (yep, that works with mixins too!) and might contain utility functions to write better mixins in the future. Here’s the code for both:

wrapping_ = (klass) ->
  getmetatable(klass).__call = (cls, self, ...) ->
    setmetatable self, cls.__base
    cls.__init self, ...

  klass

class Mixin
  new: =>
    info = debug.getinfo 2
    file = string.match info.source, "@%.?[/\\]?(.*)"

    @module = info.source\match "@%.?[/\\]?(.*)%.%a+"
    @module = @module\gsub "/", "."

    if WATCHER
      WATCHER\register file, @

  reload: (filename) =>
    print "reloading #{@module}..."

    package.loaded[@module] = nil
    new = require @module

    setmetatable @, new.__base

  find_tag: =>
    layer = @
    while not layer.tag
      layer = layer.parent

      if not layer
        return nil

    layer.tag

{
  :wrapping_,
  :Mixin
}

By the end of the three day “test phase”. This is how the first scene looked in-game:

Here’s psdscene.moon, wrapping most things mentioned in this article:

artal = require "lib.artal.artal"

class PSDScene
  new: (@scene) =>
    @reload!

    if WATCHER
      WATCHER\register "assets/#{@scene}.psd", @

  load: (name, ...) =>
    _, mixin = pcall require, "game.#{@scene}.#{name}"
    return mixin if _ and mixin

    _, module = pcall require, "game.#{scene}"
    return module[name] if _ and module[name]

    _, mixin = pcall require, "game.common.#{name}"
    return mixin if _ and mixin

    _, module = pcall require, "game.common"
    return module[name] if _ and module[name]

    LOG_ERROR "couldn't find mixin '#{name}' for scene '#{@scene}'"
    nil

  reload: (filename) =>
    filename = "assets/#{@scene}.psd" unless filename
    print "reloading scene #{filename}..."

    @tree, @tags = {}, {}
    target = @tree
    local group

    indent = 0

    psd = artal.newPSD filename
    for layer in *psd
      if layer.type == "open"
        table.insert target, layer
        layer.parent = target
        target = layer
        LOG "+ #{layer.name}", indent
        indent += 1
        continue -- skip until close
      elseif layer.type == "close"
        layer = target
        target = target.parent
        indent -= 1
      else
        LOG "- #{layer.name}", indent
        table.insert target, layer

      cmd, params = layer.name\match "([^: ]+):(.+)"
      switch cmd
        when nil
          ""
        when "tag"
          @tags[params] = tag
          layer.tag = params
        when "load"
          params = [str for str in params\gmatch "[^,]+"]
          name = table.remove params, 1

          mixin = @load name
          if mixin
            LOG "loading mixin '#{@scene}/#{name}' (#{table.concat params, ", "})", indent
            mixin layer, unpack params
          else
            LOG_ERROR "couln't find mixin for '#{@scene}/#{name}'", indent
        else
          LOG_ERROR "unknown cmd '#{cmd}' for layer '#{layer.name}'", indent

  update: (dt, group=@tree) =>
    if group == false
      return

    for layer in *group
      if layer.update
        layer\update dt, @\update
      elseif layer.type == "open"
        @update dt, layer

  draw: (group=@tree) =>
    if group == false
      return
    elseif group == @tree
      love.graphics.scale 4

    for layer in *group
      if layer.draw
        layer\draw @\draw
      elseif layer.image
        {:image, :ox, :oy} = layer
        love.graphics.setColor 255, 255, 255, layer.opacity or 255
        love.graphics.draw image, x, y, nil, nil, nil, ox, oy
      elseif layer.type == "open"
        @draw layer

{
  :PSDScene,
}

Seeing that everything was (and is) going very smoothly up to this point, I decided to “end” the test phase and finalize the decision to roll out my own engine.