Doing more with less

Overview


When a programmer is thinking about making an RPG, the immediate question arises: "How am I going to make/implement/deal with all that content?"

RPGs, though seemingly simple in their mechanics, are actually very content heavy. They require lots of items, equipment, monsters, sprites, dialog, special effects, sound effects, and thematic music. Moreover, because RPGs are actually quite simple mechanically, and are largely story driven, I would argue that they tend to need even more polished content than your average game.

As mentioned in my Intro post, because of the existing Sonic fan game community, I don't have to make all this content myself and although that helps a lot, this is still quite a large undertaking.

Many RPG developers who don't have teams of people to work with will tend to take this problem and go "full-programmer on it", by creating as much content procedurally as possible (e.g. procedural enemies, dungeons, items, etc). I'm not really interested in that. I want this data to be crafted carefully and thoughtfully and feel deeply connected and meaningful to the world it inhabits. For this reason, my approach to RPG content will be centered around how to more easily craft and iterate on the creation of content, and not how to best procedurally craft instances of content.

Here are some of the things I'm using/doing to achieve that goal...

Code as Data


For this project I'm using the Love2D game engine. Simple and elegant--but most importantly--well documented.

One of the things I love about Love2D is that it uses the Lua scripting language. Lua is a dynamic language that is very minimal on syntax while still being extremely expressive. As a scripting language, lua can read in lua files, and will always require a file's content at-most once. So rather than store data for items or player stats or enemies or equipment or special moves as json or yaml files, it's actually highly beneficial to store this data as a lua script.

Check out what a player definition looks like in lua script:

local Fun = require "util/EasingFunctions"

return {
    id = "sonic",
    name = "Sonic",

    avatar = "avatar/sonic",
    sprite = "sprites/sonic",
    battlesprite = "sprites/sonicbattle",

    startingstats = {
   
     startxp = 0,
        maxhp   = 950,
        maxsp   = 10,
        attack  = 8,
        defense = 7,
        speed   = 10,
        focus   = 5,
        luck    = 4,
    },

    maxstats = {
   
     startxp = 95000,
        maxhp   = 10000,
        maxsp   = 100,
        attack  = 80,
   
     defense = 70,
        speed   = 100,
        focus   = 80,
        luck    = 40,
    },

    growth = {
   
     -- Note: t = normalized level (level/MAX_LEVEL_CAP)
        -- Formula = startingstat + fn(t) * (maxstat - startingstat)
        startxp = Fun.quad,
        maxhp   = Fun.linear,
        maxsp   = Fun.linear,
        attack  = Fun.linear,
        defense = Fun.linear,
        speed   = Fun.linear,
        focus   = Fun.linear,
        luck    = Fun.linear
    },

    equip = {
   
     weapon    = require "data/weapons/Gloves",
        armor     = nil,
        accessory = require "data/accessories/Backpack",
    },

    items = {
   
     {count = 1, item = require "data/items/GreenLeaf"},
        {count = 1, item = require "data/items/CrystalWater"},
    },

    levelup = {
 
       [1] = {
            messages = {},
            skills = {
                require "data/battle/skills/Spindash"
            }
        },


        [3] = {
            messages = {"Sonic learned \"Bounce\"!"},
            skills = {
                require "data/battle/skills/Spindash",
                require "data/battle/skills/Bounce",
            }
        },
    },

 
   specialmove = require "data/specialmoves/sonic",

    battle = {
        require "data/battle/SonicHit",
        require "data/battle/Skills",
        require "data/battle/Items",
    }
}

With the above example you can see the numerous benefits lua is giving me in data definition:
  • Data format is intuitive and human-readable
    • Even more so than json I would argue
  • Can put comments in data
  • Can require files to share and reuse data from other files
    • Fast and efficient, since it relies on lua's own module system
    • File requires at-most once
    • Allows us to have richer data that may be lazy or functional rather than a flat value (See growth field)
  • Loading data from lua scripts into Love2D is extremely fast since it is merely interpreting the lua file as it would any other script
    • Data is immediately inflated upon execution, as you would expect
    • No special serializing or deserializing necessary

This little trick (Which is honestly pretty common for people who use Love2D) has really simplified the process of building up complex and interrelated data that RPGs absolutely require.

Animate Everything (procedurally)


One of the things modern games tend to have, especially RPGs, is a lot of bounce to things. Everything tends to be animated in some way, and I find that these little details and tweens make a huge difference when playing a game. So if my goal is that everything should be animatable, and that a lot of that animation should be procedural, what does that look like?

Traditionally, when building a game, you have code running in a loop and each iteration of that loop you check the state of things and perform actions based on the snapshot you are currently observing. Now, if you want to animate things, what that means is that you want to define what your update function "will do" over the course of several frames, as opposed to just engaging with the current frame you're on. Enter the concept of "Action Lists", which is an approach for defining a concept which breaks down into multiple single-frame actions to be executed each iteration of your update loop.

For instance, let's say you wanted your player to play a sprite animation called "walk" while moving downwards 10px. After that, you want the player to blink green three times.

With my Action List system, that would look like this:

scene:run(Serial {
    Parallel {

        -- Play "walk" animation for player sprite
   
     Animate(pSprite, "walk"),

        -- Linearly interpolate player sprite from
        -- current y pos to y + 10
        Ease(
            pSprite.transform,
            "y",
            pSprite.transform.y + 10,
            10,
            "linear"
        )
    },

    
-- Quadratically interpolate color from "very green" and
    -- back to normal green again, 3 times
    Repeat(
        Serial {

            Ease(pSprite.color, 2, 500, 10, "quad"),
            Ease(pSprite.color, 2, 255, 10, "quad"),
        },
        3

    )
})

The reason this is such a powerful way of implementing animation logic is because it is:

  1. Concise
  2. Declarative
  3. Expressive

These three concepts make animations both relatively easy to grok and easy to tweak, allowing us to layer in complexity (Either parallel animations or animation dependencies) with relatively little effort.

Action Lists also give us a nice way of describing logic for asynchronous loading, such that you have an Action per asset, waiting for a response from your asset thread. You can then have your loading screen simply put all your "Task" Actions under a Parallel Action, alongside some sort of nice loading animation.

local tasks = {}
for _, assetName in pairs(assetsToLoad) do
    table.insert(tasks, Task(assetName))
end

scene:run(Serial {
    -- While waiting for tasks, show loop loading animation
    Parallel {

        Parallel(tasks),
        Animate(scene.bgSprite, "loading")
    },

    -- After all assets are done loading, go to next level
    Do(function() sceneManager:nextLevel() end)
})

Time dialation


One nice thing about Action Lists is that they are controlled using your update loop's time step. This means you can slow down or speed up Action Lists by passing multiples of dt (Delta time) to the update function of your Action List. Right now, I have this hooked up so you can hold "s" to slow down the Action Lists or "f" to fast forward them.

If you are creating a complex new animation, slowing down your Action Lists is a handy way of spotting minor errors. If you are trying to move quickly through parts of the game without adding special teleports or level skipping, it can be handy to fast forward your Action Lists. It's a dead simple feature to implement and pays dividends almost immediately.

Tiled (Tile Editor for creating levels)


During my education as a programmer, I spent a lot of time building tools like Tiled from scratch, but doing that is kind of a pain, so I am really glad this tool exists. Not only does Tiled feature a highly versatile editor for 2D tile data, it also has layers, objects, metadata, and a Love2D exporter that produces a lua file containing all of your map data. I really can't recommend this tool enough, it's exactly what I needed and saved me a lot of effort. If this sounds like a tool that would be useful for you, check it out-- and toss them a donation!


Aseprite (Sprite Editor for creating art)


This other tool is something I never knew I needed-- a sprite editor. If you want to make high quality sprites and sprite animations, using standard MS Paint-style programs will only take you so far.


Aseprite provides you with a ton of sprite-art specific tools that are really cool, including:
  • Pixel-zoom
  • Grid view
    • Great for creating atlases and creates a selectable window context so you can dbl-click and copy/paste frames from an atlas
  • Color indexing
    • This is such a cool concept. It creates a color map based on your image
    • Great for testing out color schemes-- change the color index and it will affect every pixel of your image!
  • Pixel blurring
    • Great for editing more detailed background art without a ton of effort
  • Random pixel shifter
    • Great for quickly creating chaotic vfx like fire
  • Layers and opacity settings
    • Perfect for taking screenshots from the show and tracing pixel art over the image
      (This is what I did to make the Rover bot art)
  • Sprite animation tools
    • I don't know how anyone creates sprite animation without a tool like this
    • I can just watch and tweak and watch and tweak--the workflow is absolutely amazing
  • More features that I'm too n00b for
For making a 2D sprite game, this tool is really life changing. There's a trial version, but I highly recommend just buying it. Get it!


That covers it for this post, I may go back and talk about some of these things in more detail.

Comments

Popular Posts