Thumper (v0.0.1)

(Lil’) Thumper

Super early access to a top down puzzle game about hitting the ground hard to make sound (but also maybe that isn’t such a good idea).

silly little video of the first playable version

Inspired by games like Hoplite but highly restricted by my own abilities and the display size of the Norns, Thumper is the fun, playable demo for my attempt to whip up a little game engine (with sound!!) from scratch on the Norns.

Requirements

  • Norns
  • Patience as this develops

Documentation

These controls may change slightly over time – be sure to check the script notes for specifics on the version you are trying to use.

  • Rotate LT (lil thumper, not lieutenant) with E2
  • Hop diagonally (see LT’s lil head? he hops that-a-way) with K2
    • like that hopping animation? I’m iffy on it… but it looks cool!
  • Hold down K1 to toggle a ruler (currently referred to in the code as a grid, which will be changed for obvious disambiguation reasons).

Contributing

I’m a super beginner but I have lots of drive and free time lately. This post is kind of my personal call to action but I’d love some collaboration or just help! Read the TODO, answer some questions, DM me… whatever!

TODO

Short Term
  • finish tile rendering and expand tile materials/types
  • add tile/player collision
  • render a basic room with walls
  • make floor tiles that are breakable
  • make an exit tile that triggers a win condition
  • add a flip (and slam) move which damages floor and stone tiles
    • stone tiles degrade into floor tiles which degrade into nil tiles
  • if you move onto a nil tile, you die and it resets
  • the initial demo will be complete when it is challenging to navigate to the exit without breaking your pathway there in the process
Medium Term
  • implement a gameplay loop within the script? rn the script is the loop?
  • basic SFX via Timber
    • if left to my own devices I’l probably go the freesound route for this early stage… unless someone wants to submit sound fx to use?
    • I’m also working on a little tracker with features from EdLib and SID Wizard and am planning to use Timber currently. If this changes, you will know :slight_smile: Can’t wait to see the official Norns tracker so I can steal stuff from it to make my own tracker work… I still anticipate needing it as it’s got many features that make composing little loops and one-shots easy to do. Not all trackers these days focus on compressing compositional and sample data quite as well as old weird chip trackers!
  • SFX triggers for collision and movement
    • is my habit of relying on things like ‘screen’ being global and calling them from within my modules a bad one? otherwise just writing default SFX triggers with Timber globals seems like the way to go
  • general code cleanup (in general I’m saving a lot of this for after a playable demo with all features mentioned above)
Long Term
  • menu and pause screens (timed rooms are part of the idea behind this being a rhythm game in disguise)
  • a larger character design for zoomed in cutscenes (or maybe some minigames with a different rendering scheme?)
  • level selection and other options via params
  • a plot, which will be worked on by myself and an old writer friend. Need to know basis.

Download

(download to come when the demos become fun)

lil’ thumper on GitHub if you want to check it out or contribute!

26 Likes

update: I’d forgotten to push my final changes, so please be sure to refresh if you’re checking it out right now :slight_smile:

I’ve been hoping someone would start making games for norns! Looking forward to the day someone figures out how to get Doom running on it

2 Likes

side note: any efforts towards running Norns and getting a wee Cairo surface displaying would be… much appreciated. I’ll be monitoring those threads but also wanted to just say something here?

non-update update – im pluggin in vector.lua and rewriting :slight_smile:

1 Like

Got tile rendering in a small test script working – but have run into something interesting but not unexpected:

  • blitting tiles out from a list of pixel levels (my existing method) might be kind of slow… I’m seeing a decently big increase in time to draw each tile and (caution about premature optimization to the wind) I’m wondering how I might reclaim some of that time.
    • idea 1: don’t create 8x8 (world scale * world scale) grids of rectangles, all filled individually. Do sort those rectangles by level and draw them in groups, filling each different brightness as you go.
    • idea 2: tile designs made of a handful of shape primitives.
    • idea n: crushing the number of levels to fill and using shape primitives with minimal pixel shading to maximize performance (combo of 1+2 and some extra optimization beecause this is plan c – currently)

I feel like I need to be more comfortable with this tile rendering system before I merge it into lil_thumper… So I’m going to try implementing these and run more tests! Updates to come, as always :slight_smile:

1 Like

Update!

  1. Originally it took 0.07 seconds to draw 16x8 tiles of 8x8 pixels. This process rendered 44 boundary tiles with a stone texture, and the rest with a plank texture.

  2. Rather than storing the 8x8 pixel grid, I stored a table of lists of vectors containing the coordinates grouped by brightness level. Here’s the code:

-- expects an 8x8 table of integers between 0-15
local function px_to_drawtable(p)
    local t = {}
    for j = 1,8 do
        for k = 1,8 do
            local z = p[j][k]
            if t[z] == nil then
                t[z] = {}
            else
                table.insert(t[z],vector(j,k)) 
            end
        end
    end
    return t
end
  1. This got me down to 0.04 seconds per paint, with a couple of caveats:

    • The improvements here will be inconsistent when comparing tiles with different brightness ranges. I don’t mind this because I’m planning on leaving brightness levels 10-15 for sparse highlights and more important ‘sprites’.
    • It’s still pretty taxing – capable of under-volting my Norns Shield with a less-than-ideal adapter. I also got some lockups when I hard capped the refresh rate to 30, so we’re looking at either no continuous animation or a low framerate. Not unexpected, tbh. Kind of a fun design constraint.
  2. Conclusions on the Day:

    • shouldn’t be that hard to get lil_thumper looking real pretty this weekend. Maybe I can fit collision in too :slight_smile:
    • my timeline being really backloaded on the medium-to-long-term with audio stuff was a good call after all! The Norns graphical capability for something like this is at least limited and at most a tough nut to crack.
    • I’ll probably be keeping a fork of Timber that allows plugging in other stuff… I’m going to lean heavily on sound design to convey a lot of information and just playing samples might not cut it…
5 Likes

This is a cool project; thanks for logging your progress here!

In case it helps: I’ve spent the last few mornings & evenings troubleshooting slowness in a sequencer I’m working on, and I was able to speed things up a lot by avoiding the creation of new tables, instead reusing existing ones as much as I could. The more tables you create & discard, the more frequently Lua’s garbage collector has to run, and I found that was slowing my script down significantly. (I was also doing a bunch of stuff with closures, which were even worse memory-wise). That might have nothing to do with what’s slowing down your drawing routines, but just in case…

And if you haven’t already read it, the lua-users wiki has a good section on optimization.

1 Like

+1 here, definitely want to watch initialization

here, lua author discusses being bitten by too many allocations.

2 Likes

Super helpful advice! I just checked and:

  1. All init happens during init – if we’re just talking about table initialization
  2. I’m only allocating single value, local vars in my drawing functions where those values are being reused in multiple calls before the function exits. I’m assuming this is useful? I’d expect a compiler to maybe catch that but I figure in an interpreted language this is a bit of legwork I benefit from doing myself?

like for example, in snippet above (i don’t see it in the repo)

  local t = {}
  for j = 1,8 do
      for k = 1,8 do
           ...
                table.insert(t[z],vector(j,k))

i think it would be better to pre-allocate t with 64 slots? since it’s a temp variable it could even be a “static local”? (local, but allocated in whatever parent module of this function)

just a thought, but - that kinda thing. didn’t have any specific criticism, it looks good! just recommending the optimization article as a fun and informative read.

1 Like

Oh, def – I also should probably create a single tile once, then copy that tile during init eventually.

How would I pre-allocate t in this case? I think I’d have to figure out how many pixels there are at each brightness level and feed those in as parameters… but that seems like way too much work?

Also, thanks for just having a look <3

ah, sorry yeah - i missed the context. i now grok that t is supposed to be sparse. each t[z] contains all the points that need to be drawn at a given brightness, yeah?

so, if we are really gonna be extreme about this (we’ve profiled the heck out of this, and we know that optimizing px_to_drawtable is crucial and worth burning some MB of RAM… (we only have one of these)


-- a brightness table shall have: a count of pixels, and a table of pixel vectors

local function level_table_new(max_pixels)
  if max_pixels == nil then max_pixels = 128 * 64
  local t = {}
  t.count = 0
  for i=1,max_pixels do
    t{i} = {x=0, y=0} 
  end
  return t
end

local function level_table_clear(level_tab) 
   level_tab.count = 0
end

local function level_table_add_px(level_tab, x, y)
   level_tab.count = level_tab.count + 1
   level_tab[level_tab.count].x = x
   level_tab[level_tab.count].y = y
end



function clear_drawtable() 
    for i=1,64 do 
        level_table_clear(drawtable[i])
    end
end 

local drawtable = {}

function drawtable_init()
  for i=1,64 do drawtable[i] = level_table_new() end
end

drawtable_init()

function bitmap_to_drawtable(bmp) 
    clear_drawtable()
    for i=1,8 do for j=1,8 do
        level_table_add_px(drawtable[bmp[i][j]], i, j)
    end end
end 

now, i have no real clue if that will be faster or not, it’s just something i would try - no allocations and no conditionals in the hot function.

but really, the answer to these things would be to have more drawing routines available. e.g. being able to make a cairo surface from a pixel array, persisted in RAM.

1 Like

Yeah that would be a ton of fun… and a real headache I assume. For now I’ll dare to dream with this hacky rendering method.

As for the status quo: I think I’m seeing that your code takes optimization one level further than I did – I’m still calling a draw function per tile; world.draw() just ipairs through world.tiles() (none of this code is posted yet, hoping to do that tomorrow night).

I think that maybe I could do it ‘Grim Fandango’ style and bake static things like borders into a big level_table. Essentially a ‘background layer’, I guess. And each dynamic tile is a ‘sprite’?

For reference, here is the function that gets called when you draw a stone tile (which has 3 hit-points and a drawTable for each):

function stone:draw()
  local ox,oy = self.offset:unpack()
  for level,coords in pairs(self.drawTable[self.hp]) do
    screen.level(level)
    for _,v in ipairs(coords) do
      local x,y = v:unpack()
      screen.pixel(x+ox,y+oy)
    end
    screen.fill()
  end
end

Doubleposting because lots of new info:

There is a function that allows a segment of memory to be granted ‘surface’ status… But if you look at the long answer in this thread you’ll see that it’s a PITA.

I wonder though – could the surface data be copied after being slow-drawn once and then reused as a starting surface? I’m not super familiar with Cairo. Maybe I should post in the development thread?

Update:

Not a new version because no code yet; but for one I’d like to welcome @Snail who is not only a new user here but an old friend of mine from high school!

I’m including some crude design documents because house hunting (after nasty landlord calls) during this time is… exhausting. I took an artistic approach! – What follows is just a brain dump while bf Kai makes kimchi fried rice and scallion pancakes!


My buddy (@Snail) is getting cozy with the platform (he now has a factory Norns) while I move, then we’ll be implementing a side scrolling, tempo-based, performance/rhythm game… ideally a guitar hero platformer with branching paths that lead to different musical results.

The reasons for this are outlined below:

  • Rendering at speed is highly limited by Lua’s single threadedness and my own non-ability to write good C code. I’m practicing, but it will be a while :slight_smile:
    • for this reason, rendering is split into two modes:
      • the original mode used in my tile demo (soon to be uploaded)
      • a faster, fog-of-war render that ignores tiles far away from the player
    • ideally, players will use the first mode as exploration to practice
    • then, the second mode is switched on when level is performed and becomes a side scroller that scrolls at a rate according to the bpm. The original test tune will probably be at 96 bpm and 120 bpm
      • as an additional challenge (and a limitation of the platform) this faster rendering mode will render only a certain number of tiles in the surrounding radius. Ideally, I’ll be able to add LOD options for drawing tiles that are approaching using only a handful of calls instead of drawing expensive sprites. I’m confident in this approach even though I don’t have much code yet! We’ll see if that pans out…

pics for your imaginations~
thumper-designdoc-200418.pdf (1.5 MB)
thumper-designdoc-collision-200418.pdf (1.4 MB)


P.S. I am full of rice and pancakes and here is a clarification/addendum:

  • rather than if/thens in the :draw() methods, redraw() will trigger whatever function is present at some_object:draw()
  • move() (at least publicly for input purposes etc) optionally takes a vector and a string: the vector says where to move, the string is used to look up a function and assign that function to :draw().
  • it sounds crazy to me, but I’m going to probably try having the draw() methods share a single counter and be programmed to naturally flow to each other according to the diagram in the first pdf – everything always settles back to standing, even if there is a constant (1,0) sidescroll vector pushing you forward every beat… or a (0,-1) gravity about to cause you to plummet to your death!

Hope all is well with everyone who is keeping up with this! <3

4 Likes

Okay, how about this – everything between screen.clear() and screen.update() lives in a coroutine that runs a while loop; all drawing code is declared local to that coroutine once during screen_init().

redraw() (and the usual screen_dirty logic traps in the beginning) becomes:

  1. clear the screen
  2. resume the drawing coroutine, passing an array of updates to game state
  3. wait for yield
  4. update the screen and set dirty to false

Am I misunderstanding coroutines and their relationship to the value of making things local?

I have a demo maze drawing program I’m working on right now in some unexpected down time. Might try to implement this drawing technique here and report the results!

Another weird optimization idea I had: Lua forces strings to be immutable… what if I stored pixel data in strings? Provided an efficient way to unpack them, it’s an idea for guaranteeing efficient memory usage, lol.

1 Like

New demo! Took a few days to whip together my best push at an engine for now:

maaze (WIP)

1 Like

Mostly just have time for updates and private sketching while I move, but here’s the skinny:

I’m almost done with everything I wanted to get out of these early demos. I’ve got a little stack going and the work is about done: just a bunch of returns to be passed now :stuck_out_tongue:

3 - finish my little maze drawing thingy (potentially using a coroutine scheme i’ve nicknamed the crayon box, potentially just using what i learned from it’s failures)

2 - get my new drawing code into lil_thumper and finish the playable demo laid out in the first post of this thread!

1 - shut down this thread and move development over to whatever happens with The Bravery Stream. I’d love to keep a thread going, but I’ve found motivation beyond that and I think I can handle enough of what’s ahead (with more secretive help) to make sure that this is a fresh experience for folks. That might not end up mattering in the long run, but it can’t happen if I don’t plan and practice discipline!

1 Like