Log In  
Follow
jasondelaat
:: Unfold ::

[sfx]

I don't know if people are still having problems with the inline music player but here's a cart with the song just in case.

Cart #mibamawojo-0 | 2023-01-30 | Code ▽ | Embed ▽ | No License

I've made no attempt to optimize space and used pretty much all the sfx slots. Though some are duplicated just so if you happen to listen on the editor music/sfx tab it's kind of satisfying to watch. To me at least.

P#125018 2023-01-30 11:33

:: Unfold ::

Just a little weekend "I need a break from my projects" project.

Cart #noise_march-0 | 2022-11-26 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
1

Uses the marching squares algorithm to determine tile placement at each step and 3 dimensional value noise to animate it.

P#121419 2022-11-26 17:50

:: Unfold ::

This isn't really anything special, just a little quality of life improvement I've found handy and keep in a file of utility functions I use frequently.

Overloads sub() so it works on arrays as well as strings. I find it useful to be able to slice and dice arrays in the same way as strings and especially now that we can index strings with square bracket notation the difference between them isn't always important. I've got something with a length and I want a sub-section of it. Makes sense to use the same function for both, in my mind at least.

_sub = sub

function sub(lst, i, j)
   if type(lst) == 'string' then
      return _sub(lst, i, j)
   else
      local r = {}
      for n=i,j or #lst do
         add(r, lst[n])
      end
      return r
   end
end

Creates a new array without modifying the original. Doesn't handle negative indices because I've not really needed them so far but it would be easy enough to add.

Potentially a feature suggestion @zep? I don't mind writing the function myself but I'd also be happy not to. But maybe I'm the only one who finds it useful.

P#120538 2022-11-12 13:15

:: Unfold ::

I'm working on a game using procedural generation and although the game is nowhere near done I thought the generation technique itself was interesting enough on its own to post about.

Rather than try to explain it myself I'll just send you to someone who already has, better than I likely could. BorisTheBrave: Graph Rewriting for Procedural Level Generation

Instead, I'll just share some of my initial results. The general idea is to generate the "big picture" details first and then successively refine them to build a fleshed out so the levels generated here are shown at 1/8 scale. Eventually every pixel in these maps will expand to be one tile at full scale with additional generation filling in the fine details along the way.

Cart #nisofunajo-0 | 2022-11-06 | Code ▽ | Embed ▽ | No License
2

The rules I'm using a pretty simple for now; they were mostly intended to help me shake out any bugs in the graph rewriting system itself. The actual rules I end up using will probably be somewhat more complex but even so I think I'm already getting some decent results.

The Cycle

The first thing I do is generate a cycle with a start (green square) and an end (red square.) The idea for each level is you go in, get whatever is waiting for you at the end—treasure, a boss fight, whatever—and then have to get back out. Having a cycle gives you multiple potential routes to accomplish that.

At the moment the cycles go one-way—although it's not shown—with one path leading toward the end and the other path leading from the end back to the start. But you could have two paths heading towards the start where one might be shorter but full of enemies and the other is longer but relatively safe, and all sorts of other variations.

Terrain

Next up is terrain. For this demo I just have two terrain types, stone and water. You can have both types on a single tile giving you wet stone. The rules are written so that stone and water will never touch directly but will always have wet stone in between. This also results in the occasional patch of wet stone bordered by either stone on both sides or water on both sides which helps to vary things up a bit more.

For the actual game I'm thinking I'll just have abstract terrain types: terrain-1, terrain-2, etc. Then each level will have a randomly chosen theme and that theme will give me a terrain palette which will assign actual terrain types. So you could have fire level and ice levels and so on.

Enemies

The enemy rules are fairly straight forward. There are four types:

  1. Large enemies (big X)
  2. Small enemies (small x)
  3. Swarms (clusters of small enemies that attack together)
  4. Guardians (in a white box with a border. When you come into their
    area everything closes off and you can't continue until you defeat
    them. Basically mini-bosses.)

The red ones will be visible as you approach and the yellow ones will be hidden and ambush you. You'll never get two large enemies right next to each other but any other combination of enemies is possible.

Obstacles

Obstacles show up on the map as locks and keys but they may not be actual locked doors requiring keys. They're just anything which requires you to do or obtain something in one part of the level in order to progress in another part. The "key" could be an actual key but it could also be a lever or a puzzle or a new ability. Maybe you need to drain water in one area to reveal a door in another. Whatever.

This, in particular, is an area where I think graph rewriting shines. Since at this stage the whole level is represented as a graph and the path from start to end and back to start is a directed sub-graph, it's relatively easy to ensure that keys never end up behind their associated lock. The lock and key are initially spawned on the same tile with keys optionally then being moved backwards along the path but never past the start.

At the moment there's a 50% chance of a key moving on each iteration of the rules so, on average, keys are found fairly close to their locks but I can tweak the probabilities to have them move either more or less.

Rooms

Rooms are added to empty areas near the main path. Rooms are either big or small and once placed one or more doors are added to the room connecting it to other rooms and/or the main path.

The dark gray bars connecting the rooms to the path are just to show roughly where doors are and how they connect up to the main path. There won't be literal corridors to every room. Either the rooms will be moved closer to the main path or the surrounding terrain will be expanded to the edge of the room.

At the moment the rooms are all just kind of plopped down on top of each other. Whether overlapping rooms merge into a bigger room or where exactly the walls between them will be will be figured out at a later stage.

Next steps

Next is to take this simple set of rules and and basic level layouts and turn them into full-fledged level maps to get the basic infrastructure in place to handle that process.

After that I'll probably take a break to work on actual game mechanics before really diving into the level generation rules.

P#120219 2022-11-06 19:31

:: Unfold ::

A few days back @dw817 posted a thread about the number of possible combinations in 256 bytes in which they asked:

> Is there a way to calculate 256^256 ?

And knowing there are all sorts of tools and languages which can handle such huge numbers I cheekily posted the answer and caused a bit of a derail.

Anyway, I had some time on my hands so I figured I'd implement large number support for Pico-8.

Cart #jd_bignum-1 | 2022-11-05 | Code ▽ | Embed ▽ | No License
3

Edit: Fixed a bug in divide()

Is this likely to be of use to anybody? Probably not. Is it major overkill? You betcha! Though in fairness, at 706 tokens it came out smaller than I was expecting.

Features/Functions

  • bignum(n): Create a 'bignum' with value n which is just a regular Pico-8 number. Most operations require both numbers to be bignums. So bignum(16000)*bignum(1254) but not bignum(16000)*1254.

    bignum() is equivalent to bignum(0)

  • divide(bn): You can use the division and mod operators (/, %) on bignums if you only need one or the other. The divide function returns both the quotient and remainder at once.

    q, r = divide(bignum(3)^16, bignum(2)^15)

  • show(bn): Converts a bignum into its decimal representation as a string.
  • factorial(n): Calculates the factorial of n and returns the result as a bignum. Factorials get very big, very fast so the input to factorial is just a regular number.
  • + - / % ^: Arithmetic operators behave how you would expect. All require both arguments to be bignums with the exception of ^ where only the first argument is a bignum and the second is a regular number. Division is integer division only so bignum(3)/bignum(4)==bignum(0).
  • > >= <= < ==: Comparisons similarly require both arguments to be bignums.

Issues

  • The arithmetic operators are all fairly efficent but probably not optimally so.
  • I wasn't taking negative numbers into account at all so I make no promises about what will happen if you try to use them. Maybe it will work! (It probably won't work.)
  • show is extremely inefficient. I'm sure there are better ways to convert very long binary strings to decimal but I'm not familiar with them so this is what I've got. Large numbers (like 52!) may take a bit of time and very large numbers will crash the program as it runs out of memory. (bignum(256)^256 does this, sadly. It calculates the number fine but crashes when you try to convert it.)
P#120151 2022-11-05 13:35 ( Edited 2022-11-05 20:45)

:: Unfold ::

I know a lot of people, myself included, usually write their pico~8 code a little off the cuff tinkering with it until it works. Which tends to be more fun in my experience. But it can also be incredibly frustrating if I'm working on sometime more complex where every time I change something I break something else. And in those cases planning out some formal tests helps me maintain my sanity and get the thing actually working much faster than I probably would otherwise. And since I'm working on something fairly complex at the moment, I took a bit of a detour and put together a little test framework and thought I'd make it available for anybody else who might find it useful.

The code is on github: https://github.com/jasondelaat/pico8-tools/tree/release/testo-8
Or you can just copy it here:

--------------------------------
-- testo-8: testing framework
-- copyright (c) 2022 jason delaat
-- mit license: https://github.com/jasondelaat/pico8-tools/blob/release/license
--------------------------------
do
   local all_tests = {}

   local function smt(t, mt)
      return setmetatable(t, {__index=mt})
   end

   local function shallow_copy(lst)
      local copy = {}
      for l in all(lst) do
         add(copy, l)
      end
      return copy
   end

   local function filter(f, lst)
      local results = {}
      for l in all(lst) do
         if f(l) then
            add(results, l)
         end
      end
      return results
   end

   local execute_meta = {
      execute=function(self)
         local result = self[4](self[3](self[2]()))
         if self._cleanup[1] then
            self._cleanup[1]()
         end
         return {
            result,
            self[1]..self.when_txt..self.result_txt
         }
      end
   }

   local when_result_meta
   local result_meta = {
      result=function(self, txt, fn)
         local t = shallow_copy(self)
         t.when_txt = self.when_txt
         t.result_txt = 'result '..txt..'\n'
         t._cleanup = self._cleanup
         add(t, fn)
         add(all_tests, smt(t, execute_meta))
         return smt(self, when_result_meta)
      end
   }

   local _cleanup
   local when_meta = {
      when=function(self, txt, fn)
         _cleanup = {}
         local t = shallow_copy(self)
         t.when_txt = 'when '..txt..'\n'
         t[3] = fn
         t._cleanup = _cleanup
         return smt(t, result_meta)
      end
   }

   when_result_meta = {
      when=when_meta.when,
      result=result_meta.result,
      cleanup=function(self, f)
         add(_cleanup, f)
         return self
      end
   }

   local given_meta = {
      given=function(self, txt, fn)
         local msg = self[1]..'given '..txt..'\n'
         return smt({msg, fn}, when_meta)
      end
   }
   function test(name)
      _cleanup = {}
      local t = smt({name..':\n', _cleanup=_cleanup}, given_meta)
      return t
   end

   local function run_tests()
      cls()
      cursor(0, 7)
      local results = {}
      for t in all(all_tests) do
         add(results, t:execute())
      end
      local failures =
         results and filter(function(r) return not r[1] end, results) or 0
      if #failures == 0 then
         print('all '..#all_tests..' tests passed!', 0, 0, 11)
      else
         for f in all(failures) do
            print(f[2])
         end
         rectfill(0, 0, 127, 6, 0)
         print(#failures..'/'..#all_tests..' tests failed:\n', 0, 0, 8)
         cursor(0, 127)
      end
   end

   function _init()
      run_tests()
   end
end
-- end testo-8 ------------------------------

And here's a cart with some simple functions containing errors and a few tests, most of which fail just to give you an idea of how it works.

Cart #testo8_demo-0 | 2022-10-23 | Code ▽ | Embed ▽ | No License
1

Testo-8

The framework is pretty simple. It's just a single function test which returns an object exposing methods— given, when, result and cleanup —for defining the test.

Testo-8 defines an _init function which automatically runs the tests. Just #include testo-8.lua, write tests and run the cart. If you've defined your own _init you'll probably need to comment it out to get the tests to run.

A very simple test might look something like this:

test('some simple addition')
    :given('the number one', function() return 1 end)
    :when('adding 2', function(n) return n+2 end)
    :result('should equal 3', function(r) return r == 3 end)

The methods given, when and result —which I'll call clauses—all take a string as their first argument and a function as their second, while test takes a string argument only. The strings are used to build the output message if the test fails.

The function arguments taken by the other methods serve different purposes:

  • given should return the object(s) being tested. (1 in the example)
  • when takes the object(s) being tested as input and does something with it returning the result(s) (add 2)
  • result takes the result(s) and should return a boolean, true if the test passes and false if it fails. (does 1+2 == 3?)

Each test has exactly one given clause below which will be one or more when clauses. Each when clause contains one or more result clauses and can optionally be ended with a cleanup clause. More on that later. So an actual test might look something like this:

-- the ...'s are just a placeholders for some appropriate function
test('some test')
    :given('an object to test', ...)
    :when('1st when', ...)
    :result('result 1', ...)
    :result('result 2', ...)
    :result('result 3', ...)

    :when('2nd when', ...)
    :result('result 4', ...)
    :result('result 5', ...)
    :result('result 6', ...)
    :cleanup(...)

    :when('3rd when', ...)
    :result('result 7', ...)
    :result('result 8', ...)

The number of result clauses is the actual number of tests that will be run so the above example would be eight tests. Each result clause is executed as follows: The given clause is executed to generate the object(s) to test. The test object(s) are passed to the when clause which appears above the result and finally the results are passed to the result clause which determines whether the test passes or fails.

So in the above example the given clause will run eight times, once for every result. The first when clause will be called three times and so will the second while the third when clause will only be called twice.

cleanup takes a single function as its argument and is used to clean up after a test if, for instance, the test modifies some global state which needs to be reset in-between tests. The cleanup clause is optional but if it exists will be called after each result clause inside the same when. The cleanup in the above example would therefore be called three times, once after each of results 4, 5 and 6.

For anyone interested in seeing actual tests, here are some I wrote for a simple s-expression parser.

function test_given(s)
   return s, function() return s end
end

function len(n)
   return function(lst)
      return #lst == n
   end
end

function index_eq(n, s)
   return function(lst)
      return lst[n] == s
   end
end

function is_table(r)
   return type(r) == 'table'
end

function is_nil(r)
   return r == nil
end

test('empty s-exp')
   :given(test_given('()'))
   :when('parsed', parse)
   :result('should be a table', is_table)
   :result('should have 0 elements', len(0))

test('single element s-exp')
   :given(test_given('(atom)'))
   :when('parsed', parse)
   :result('should have 1 element', len(1))
   :result('element should be atom', index_eq(1, 'atom'))

test('multi-element s-exp')
   :given(test_given('(a b c d e)'))
   :when('parsed', parse)
   :result('should have 5 elements', len(5))
   :result('1st should be a', index_eq(1, 'a'))
   :result('2nd should be b', index_eq(2, 'b'))
   :result('3rd should be c', index_eq(3, 'c'))
   :result('4th should be d', index_eq(4, 'd'))
   :result('5th should be e', index_eq(5, 'e'))

test('nested s-exp')
   :given(test_given('((atom))'))
   :when('parsed', parse)
   :result('should have 1 element', len(1))
   :result('element should be a table', function(r) return is_table(r[1]) end)
   :result('element should have length 1', function(r) return #r[1] == 1 end)
   :result('nested element should be atom',
           function(r) return r[1][1] == 'atom' end
          )

test('multi-element nested s-exp')
   :given(test_given('((a b) (c d) (e f))'))
   :when('parsed', parse)
   :result('should have 3 elements', len(3))
   :result('each element should be length 2',
           function(r)
              for i in all(r) do
                 if #i != 2 then
                    return false
                 end
              end
              return true
           end
          )
   :result('1st contains a and b',
           function(r)
              return r[1][1] == 'a' and r[1][2] == 'b'
           end
          )
   :result('2nd contains c and d',
           function(r)
              return r[2][1] == 'c' and r[2][2] == 'd'
           end
          )
   :result('3rd contains e and f',
           function(r)
              return r[3][1] == 'e' and r[3][2] == 'f'
           end
          )

test('multiply nested s-exp')
   :given(test_given('((a (b c)))'))
   :when('parsed', parse)
   :result('should have 1 element', len(1))
   :result('element length 2',
           function(r)
              return #r[1] == 2
           end
          )
   :result('element [1][1] == a',
           function(r)
              return r[1][1] == 'a'
           end
          )
   :result('element [1][2] is table',
           function(r)
              return is_table(r[1][2])
           end
          )
   :result('element #[1][2] == 2',
           function(r)
              return #r[1][2] == 2
           end
          )
   :result('element [1][2][1] == b',
           function(r)
              return r[1][2][1] == 'b'
           end
          )
   :result('element [1][2][2] == c',
           function(r)
              return r[1][2][2] == 'c'
           end
          )

test('empty string fails to parse')
   :given(test_given(''))
   :when('parsed', parse)
   :result('should be nil', is_nil)

test('unclosed parens fails to parse')
   :given(test_given('((a b) (c)'))
   :when('parsed', parse)
   :result('should be nil', is_nil)

test('too many closed parens fails to parse')
   :given(test_given('((a b) (c)))'))
   :when('parsed', parse)
   :result('should be nil', is_nil)

test('parsing with newlines')
   :given(test_given('(a \n    (b c))'))
   :when('parsed', parse)
   :result('should have 2 elements', len(2))
   :result('1st element should be a', index_eq(1, 'a'))
   :result('r[2][1] == b', function(r) return r[2][1] == 'b' end)
   :result('r[2][2] == c', function(r) return r[2][2] == 'c' end)

Modules and Testing

I rely heavily on an external editor and spreading all my code around a bunch of files. If that's not how you work this may not be super practical. But here's a quick run-down of how I (currently) work on a project.

Even though Pico-8 Lua doesn't technically have modules I generally try to write things in a modular way and #include with the help of do...end gives me something module-like.

A vastly oversimplified example would be something like this:

-- player.lua
local pos = {x=64, y=64}
local s = 1

local function move(var, dist)
    return function()
        pos[var] += dist
    end
end

move_left = move('x', -2)
move_right = move('x', 2)
move_up = move('y', -2)
move_down = move('y', 2)

function draw_player()
    spr(s, pos.x, pos.y)
end

Which I include inside of a do...end block like so:

Writing modules like this doesn't really cost much extra because:

  1. These are all functions I'd write anyway
  2. The local keyword doesn't use any tokens
  3. The do...end costs just a single token
  4. The added encapsulation given module local variables means I can't accidentally mess of things like the player position from other parts of my code because pos doesn't exist outside of the module.

Importantly, I don't put the surrounding do...end in the module file itself. Because when it come to writing the actual tests, I'll put those in another separate file and then include it inside the same do...end block as before.

This makes the tests part of the same module so they can access and test all the local data and functions. Once I'm sure everything is working properly I can just comment out the #include for the test file and free up all those tokens.

Issues

  1. Since Lua doesn't have exception handling capabilities like try...catch or similar, I'm not able to intercept certain errors and report them as test failures. So things like attempting to index a nil value, etc. will still cause the cart to crash and you'll have to fix those problems before the test will run.
  2. The above can also lead to occasionally cryptic error messages saying that there's an error with testo-8 itself. This is certainly possible but usually it means you've passed nil, or something else, where testo-8 is expecting a function. If you're frequently commenting out parts of your code make sure you haven't commented out a function which you're using in a test.
P#119475 2022-10-23 14:00 ( Edited 2022-10-25 18:45)

:: Unfold ::

Cart #jd_dice_wip-0 | 2022-10-16 | Code ▽ | Embed ▽ | No License

So I've started working on this. It's still very early and I've only got some of the UI elements and the very beginnings of procedural generation in place but I think there's enough to make it worth sharing.

Controls

While moving around:

  • 🅾️ to switch to the ability selector. (Coloured boxed bottom-middle)

In ability selector:

  • 🅾️ to select highlighted item
  • ❎ to cancel

Choosing 'END TURN' pulls up a 'Dice Selector' menu which doesn't do anything yet.
Press either ❎ or 🅾️ to dismiss it.

Notes

  • Actual movement in the game will be tile based but I wanted people to be able to zoom around the big empty levels fairly quickly for now because it's not like there's much to see.
  • At the moment the levels are just a big loop of straight, boring grey hallways surrounded by blackness but eventually room will be able to expand into the available empty spaces, there will be locks, keys, enemies, puzzles, etc.
  • The mini-map may or may not be in the actual game but I wanted to give an overall idea of the shape of the generated levels.
  • The green square in-level is the level entrance and the red one the level exit. Nothing happens yet when you get to them.
  • The stats on the bottom left are hard-coded and don't do/mean anything yet.

I'm trying my hand at procedural generation using graph re-writing. There's a fairly straight-forward summary of the process here for anyone interested: https://www.boristhebrave.com/2021/04/02/graph-rewriting/

This version isn't actually using re-writing yet which is why all the interesting stuff is missing from the level. It's just a graph with a basic loop. I've got the system written and—I think—working as expected so next steps are to start testing some rules for slightly more interesting levels to shake out all the bugs and then take it from there. Oh yeah, and all the actual game mechanics. Those will probably help too.

P#119177 2022-10-16 15:21

:: Unfold ::

So maybe this is old news to everybody but me but I just discovered it by accident. Turns out you can define local functions! It makes sense but it somehow never occurred to me to try before.

Here's a quick example:

do
   local function a()
      print('in a')
   end

   function b()
      print('in b: calling a')
      a()
   end

   print('in local scope')
   print(a)
   print(b)
   a()
   b()
   print('leaving local scope\n\n')
end

print('in global scope')
print(a)
print(b)
b()

Output:

in local scope
[function]
[function]
in a
in b: calling a
in a
leaving local scope

in global scope
[nil]
[function]
in b: calling a
in a

That's it. Just thought it was interesting.

P#118372 2022-10-02 18:10

:: Unfold ::

I had a bit of time to tinker yesterday. I'm not sure what made me think of the old Infocom text adventures but here we are.

If you're familiar with Text Adventures (or Interactive Fiction) you may know that one of—maybe the—most popular tool for creating them has been the Inform language. The current version is Inform 7 but waaaaay back when I was first learning, it is was Inform 6.

Anyway, I decided to throw together a quick little IF authoring API loosely based on Inform 6. It is by no means complete or particularly advanced. Basically, I followed the first tutorial for the game Heidi from The Inform Beginner's Guide and implemented just enough to make it work. But work it does! Mostly. I think...

The command parser is extremely simple so don't expect too much from it. All commands should be in the form:

verb [noun] [noun]

Where the nouns are optional depending on what you're trying to do. So east or e to walk to the east; take object to pick up an object; and put object target to put an object on/in something else. There are enough verbs, including synonyms, to complete this game and the API even lets you add new ones but don't try to be too creative with your commands. It was mostly just something to do on a Saturday afternoon so I may or may not develop it further.

Cart #jd_heidi_if-3 | 2022-09-28 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
7

The API clocks in at 926 tokens while the example game itself is only 173.

For simplicity's sake here's the code for just the game itself if you're interested in how it works. You can find the original in Appendix B of The Inform Beginner's Guide linked above if you want to see how the two versions compare.

-->8
-- example game: heidi
-- adapted from:
-- 'the inform beginner's guide'
-- https://ifarchive.org/if-archive/infocom/compilers/inform6/manuals/IBG.pdf
--------------------------------

-- defining the title screen
story(
   'heidi',
   "a simple text adventure written\nby roger firth and sonja\nkesserich.\n\nadapted to pico-8 from\nthe inform beginner's guide.\nby jason delaat.\n\n\npress enter to begin.")

-- rooms and objects
before_cottage = object('in front of a cottage')
    description("you stand outside a cottage.\nthe forest stretches east.\n")
    has(light)

forest = object('deep in the forest')
    description("through the dense foliage you\nglimpse a building to the west.\na track heads to the northeast.")
    has(light)

bird = object('baby bird', forest)
    description("too young to fly, the nestling\ntweets helplessly.")
    name('baby', 'bird', 'nestling')

clearing = object('a forest clearing')
    description("a tall sycamore stands in the\nmiddle of this clearing. the\npath winds southwest through the\ntrees.")
    has(light)

nest = object("bird's nest", clearing)
    description("the nest is carefully woven of \ntwigs and moss.\n ")
    name("bird's", 'nest', 'twigs', 'moss')
    has(container|open)

tree = object('tall sycamore tree', clearing)
    description("standing proud in the middle of \n the clearing, the stout tree \n looks easy to climb.\n ")
    name('tall', 'sycamore', 'tree', 'stout', 'proud')
    has(scenery)

top_of_tree = object('at the top of the tree')
    description("you cling precariously to the \ntrunk.")
    has(light)
    each_turn(function(_ENV)
          if contains(branch.contents, nest) then
             print('you win!')
             stop()
          end
    end)

branch = object('wide firm bough', top_of_tree)
    description("it's flat enough to support a \nsmall object.\n ")
    name('wide', 'firm', 'flat', 'bough', 'branch')
    has(static|supporter)

-- connecting the rooms
before_cottage
   :e_to(forest)

forest
   :w_to(before_cottage)
   :ne_to(clearing)

clearing
   :sw_to(forest)
   :u_to(top_of_tree)

top_of_tree
   :d_to(clearing)

-- initialization
function _init()
   location(before_cottage)
   max_carried = 1
end

Walkthrough


This example game is very short with only four rooms and a handful of objects. The goal is to return the baby bird to its nest in the tree. You can only carry one object at a time—as defined by the max_carried variable in the _init() function—but an object inside of something else counts as a single object.

in front of a cottage
--------------------------------
you stand outside a cottage.
the forest stretches east.

> east

deep in the forest
--------------------------------
through the dense foliage you
glimpse a building to the west.
a track heads to the northeast.

you see a baby bird.

> take bird

you take the baby bird.

> ne

a forest clearing
--------------------------------
a tall sycamore stands in the
middle of this clearing. the
path winds southwest through the
trees.

you see a bird's nest.

> put bird nest

you put the baby bird 
in the bird's nest.

> take nest

you take the bird's nest.

> climb

at the top of the tree
--------------------------------
you cling precariously to the 
trunk.

you see a wide firm bough.

> put nest bough

you win!

Some of the words have multiple aliases. Instead of 'bough' you could type 'put nest branch' and that would also work.

P#117587 2022-09-18 10:44 ( Edited 2022-09-28 09:38)

:: Unfold ::

[sfx]

Made for the midilib custom SFX instrument project

And in action:

Cart #jdmidi_122_seashore-0 | 2022-09-13 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
2

The animation is taken from one of my other projects. The palm tree image is adapted from the tutorial The basics of Painting with Maths by Inigo Quilez.
P#117347 2022-09-13 19:06

:: Unfold ::

I mean, who doesn't compose for helicopter, right?

Made for the midilib custom SFX instrument project

Credit's always nice but not required.

[sfx]

Here it is in action. Please forgive the poorly and hastily drawn helicopter.

Cart #jdmidi_125_helicopter-1 | 2022-09-13 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
3

P#117345 2022-09-13 10:12 ( Edited 2022-09-13 19:02)

:: Unfold ::

So this isn't strictly Pico-8 related but I just came across a video series on Youtube which I thought others here might enjoy.

The Animation of Final Fantasy

The intention seems to be to follow the evolution of FF animation all the way from FF-I up to FF-whatever-number-they're-up-to-by-the-time-he-finishes-the-series.

So far he's only up to FF-IV (the first Super Famicon/SNES release) but I think the first three (the Famicon/NES releases) are probably most interesting to Pico-8 developers. I thought it was an interesting look into how the developers managed with so few assets and—especially in FF-II—how they were able to use those assets to tell a story and convey at least some very basic sense of character.

P#117261 2022-09-12 09:53

:: Unfold ::

PICO Composer v0.3

Changes


2022-06-15:

  • Set default tempo to 120 bpm
  • Set default time signature to 4/4
  • Implemented exporter and importer code

2022-06-06:

  • Fixed display code which wasn't properly accounting for sharps causing notes to eventually be drawn offscreen.

Cart #picomposer-5 | 2022-06-15 | Code ▽ | Embed ▽ | No License
5

PICO Composer main menu

When you start the cart you'll be prompted to load an album or create a new one. Use the up/down arrow keys to navigate the menu and enter to select. Either, or both, of the tempo and time signature can be left blank and you'll be given the defaults of 120 bpm and a 4/4 time signature. Any time signature that can't be parsed properly will default to 4/4.

When creating a new album the album name will be the name of the file—no need to specify .p8 extension—where your work will be saved. At the moment each "album" can only have a single song, with a single voice, on instrument 0. The idea though is to have each album contain multiple songs. Space constraints may end up making that infeasible but that's the plan for now at least.

Controls:

PICO Composer is intended to be run locally with a full keyboard and will save your work to a separate .p8 file.

Demo Song/Album

Download this cart and save it in the same directory as picomposer.p8.png Load and run picomposer and when prompted select load album, type song6 and press enter. It should load and after a couple of seconds you'll see the last few notes of the song. Press space to play the song from the beginning.

When you export an album (press e) a new file album_name.music.p8 will be created. This file can't be edited in Pico Composer and is intended to be imported for use into your own carts. To do so copy/paste the code from the example below and use the load_music() and play() functions.

Cart #picomposer_import-0 | 2022-06-15 | Code ▽ | Embed ▽ | No License
5

(Note: The song data for this cart is in the spritesheet and it's "importing" from itself into upper memory. Usually you'll be importing from the generated .music.p8 file but I wanted a self contained example.)

The importer is currently pretty heavy sitting somewhere around 600 tokens. Once I've got a few more features implemented I'll revist and try to bring the size down.


Song 6 refers to my Tunes! thread. This demo song is the same song as the 6th (currently last) song I've posted there.

As a point of comparison: By my calculation "Song 6" as I've written it in the tracker requires 752 bytes (10 sfx patterns at 68 bytes each plus 18 music patterns by 4 bytes each) while the PICO Composer song data comes in at 750. Considering I've not yet implemented any way of repeating sections—I just had to enter every single note—that gives me hope that I'll be able to keep song sizes down.

Future Development/Feature Wishlist

  • Select/edit note(s)
  • Allow all the pico-8 instruments and custom instruments
  • Multiple songs per album
  • Dynamics: volume pp, p, mp, mf, f, ff, fff
  • Allow up to 4 voices and ability to switch around between them
  • Clefs. As is the staff is in treble clef. Adding, at least, bass clef would be useful
  • Bar lines
  • crescendo, decrecendo
  • Repeat symbols and voltas
  • Effects/articulations (tremolo, etc.)
  • Maybe a mode specifically for notating drum parts
  • Possibly the ability to convert to/from the pico-8 tracker format
  • Joining notes when possible (connected 8th notes, for instance)
  • Ties and slurs
  • key signatures
  • Draw leger lines
  • Start screen with nicer menus
  • Reduce importer code size.

Proof of concept version:


This is just a proof of concept/work-in-progress to see how much interest I have in continuing to work on it and how much (or little) interest other people might have in using it.

Here's a quick little gif demo. No sound obviously but imagine each note playing as it's entered and also on playback:

Try it out:

Cart #picomposer-0 | 2022-05-15 | Code ▽ | Embed ▽ | No License
5

Controls and (current lack of) Features

Like I said, this is currently just a proof of concept and not (yet?) a useful tool so don't expect too much from it. All input is currently via keyboard.

At the start you'll be prompted for a title, bpm and time signature. None of these matter at the moment. You're locked into 4/4 at 150bpm.

Note Entry

Note Values

The top right corner shows you what note value will be inserted next. You can change this with number keys 1-5:

  • 1 - whole notes
  • 2 - half notes
  • 3 - quarter notes
  • 4 - eighth notes
  • 5 - sixteenth notes

Pitches

To insert actual notes of the selected value use the bottom row, keys z-m, just like entering notes in the tracker with 'z' being C and 'm' being B. At the moment it's just the bottom row/white keys but I'd obviously include the sharps/flats as well in a real tool.

It's a bit awkward at the moment because the notes go up until F and then drops down to G. I did that because I didn't want to mess around with ledger lines at the moment and this way everything stays on the staff. In reality I'd have the notes all going up in a particular octave and then using the up/down arrow keys—or something else—to change which octave you're in.

I'm not using the top rows for the higher octave like the tracker does because I want to reserve those keys for additional functionality like saving, adding instruments, etc.

Playback

At any time while you're entering notes press space to play the whole song from the start. When it's done you'll be back in note entry mode.

Future Development

So that's all it does at the moment. Currently there's no way to save the song, edit notes, add rests or...anything else. But here's a probably incomplete list of things I'd probably like to add at some point in no particular order.

  • Save/load songs to memory and/or another cart
  • Octaves for a full range of notes as mentioned above
  • Clefs. As is the staff is in treble clef. Adding, at least, bass clef would be useful
  • Actually allow you to enter tempo and time signature
  • Bar lines
  • Allow up to 4 voices and ability to switch around between them
  • Allow all the pico-8 instruments and custom instruments
  • Dynamics (p, f, mf, pp, crescendos, etc.)
  • Repeat symbols and voltas
  • Effects/articulations (tremolo, etc.)
  • Maybe a mode specifically for notating drum parts
  • A way to load and play the songs in carts
  • Possibly the ability to convert to/from the pico-8 tracker format
  • Joining notes when possible (connected 8th notes, for instance)
  • Ties and slurs
  • Add rests
  • Select/edit/delete note(s)
  • key signatures

That's what I've got off the top of my head. Suggestions welcome.

P#111852 2022-05-16 10:44 ( Edited 2022-06-15 17:59)

:: Unfold ::

I may have a non-standard definition of fun...

Disclaimer: I'm fully aware that this is quite likely a completely useless little utility as far as PICO-8 development is concerned. I just like writing this kind of thing just for fun. But if you do find it interesting or useful, let me know!

This is basically just a function which allows you to create curried functions—which I attempt to explain below if you're unfamiliar with them—which can be partially applied in a natural way in Lua. I'm not sure why it took me so long to write this since it's just a rip-off of my python implementation of the same thing.

Anyway, here it is on github or you can copy/paste the code from the example.

Example

Here's a simple little proto-game thing which I stuffed full of as many curried functions as I thought I could reasonably get away with. Not gonna claim that it's the best—or even good—way to organize a game but I think it does an okay job of showing how to use partial application as a sort of dependency injection/data encapsulation.

Cart #curry_demo-1 | 2022-05-03 | Code ▽ | Embed ▽ | No License
1

What is function currying?

If you've never encountered curried functions before they can seem a bit weird at first but they're actually pretty simple.

Function currying — which gets its name from mathematician Haskell Curry for whom the Haskell programming language is also named — is a transformation which turns a function which accepts multiple arguments into a series of functions which each accept only a single argument. For instance, if we have this function which accepts three arguments:

    function sum3(x, y, z)
       return x + y + z
    end

Then when we curry it, we end up with a function which takes the first argument and returns a new function. This new function accepts the second argument and returns a third function which accepts the third argument and finally returns the result. We could write that manually like so:

    function sum3_curried(x)
       return function(y)
          return function(z)
              return x + y + z
          end
       end
    end

The curry function basically does this for you so instead of having to write a bunch of nested functions manually you could just do this:

    sum3_curried = curry(3, sum3)

Or this without having to define sum3 first:

    sum3_curried = curry(
       3, function(x, y, z)
          return x + y + z
       end
    )

Which makes it easier to see what the function is doing without having to wade through multiple levels of nested functions.

And I would want to do that because…?

Curried functions can be useful in a bunch of situations but they all basically come down to one thing: partial application.

With the sum3 function you have to pass all the arguments at once or else you'll get an error. With sum3_curried you can pass one, two, or all three and you'll always get something back. In the case of one or two arguments, you'll get back a function which you can stash in a variable and use later. In other words, you can pass curried functions some of their arguments now and the rest of their arguments at some later time.

But isn't sum3_curried(1)(2) kind of ugly and annoying to write?

It sure is!

The curry function doesn't actually construct a bunch of nested functions. Instead, the function returned by curry takes a variable number of arguments and keeps track of how many it's got so far. Once it has the right number of arguments, it calls the actual function and returns the result.

These are all valid ways of calling the versions of sum3_curried created with curry:

    sum3_curried(1, 2, 3)
    sum3_curried(1)(2, 3)
    sum3_curried(1, 2)(3)
    sum3_curried(1)(2)(3)
P#111095 2022-05-03 21:16

:: Unfold ::

Just creating a thread to collect music that I write. Mostly just for myself—to have things I like all in one place which I'll update periodically as I write new stuff—but feedback is always welcome should anyone feel so inclined. I'm mostly not making any efforts to use space efficiently. I probably should. Meh, one day. Also, I'm terrible at naming things so if anybody feels like suggesting titles or some kind of coherent naming scheme, I'm all ears.

With that in mind here's my first entry. A jaunty little tune I threw together this morning.

2022-04-16:

Song 1: Jaunty Tune in Eb Major
[sfx]

This one was from a few weeks back. Don't remember when exactly. I was going for something sort of mysterious-ish but still with some upbeat parts. Maybe a bit more repetitive than I'd like but I think turned out alright overall. C Minor, I think.

Song 2:
[sfx]

2022-04-17:

A Less Jaunty Tune in Eb Minor
This has the exact same structure as the first song but minor instead of major and with some chromatic notes thrown in to give it a bit of dissonance. I like it less but it's okay.

Song 3:
[sfx]

2022-05-09

Song 4:
[sfx]

This is another one I wrote a while ago but didn't get around to PICO-8-ifying it until today. This could be made to loop forever after pattern 07 but the way I wrote it here it repeats twice and has an actual ending.

Song 5:
[sfx]

Another older one because apparently today is my day to copy stuff over into the tracker.
Song 6:
[sfx]

2022-06-22

Song 7: Playing around with 4 part SATB writing.
[sfx]

P#110329 2022-04-16 17:26 ( Edited 2022-06-22 10:10)

:: Unfold ::

[sfx]

I've not had much time lately but thought I'd throw up a WIP that I've been playing with for a few days as a bit of a distraction from...everything. It's one of the longer pieces I've done so far at just a bit under three minutes and probably one of the first that feels like a whole piece rather than just a section or an experiment.

There's a couple rather clunky key changes which stick out like sore thumbs. Still figuring those out. Sometimes they work for me and sometimes they don't and I haven't quite figured out how to turn one that's not working into one that does. But I'm getting there. So far I'm only using 3 of the four channels so I'll probably add some percussion-y and/or other decorations in on that fourth channel at some point. Which might actually help with the key changes? Maybe? Don't know.

I really just started making music of any kind back in late October/early November. I'm pretty musically clueless so it came as something of a surprise when making music turned out to be a thing I really enjoyed in and of itself. I posted a thread (Tell me about how you compose music) to discuss making music in general and got a bunch of great replies. Thanks in particular to @packbat who gave me loads of great advice and resources. Whether I've used them effectively or not is another matter. I'd always be happy to hear from more people about their music making process if anybody feels like hopping over there and adding anything.

P#108184 2022-03-07 19:51

:: Unfold ::

I'm brand new to making music and I'm interested in hearing about how different people approach composing for their games. Not in terms of using the music editor—I'm fine with that—but more about your process/workflow for creating the music itself. You can assume that I understand about scales/modes, keys, chords, chord progressions, etc. and feel free to sling around jargon/technical terms if you want I can always go look up anything I don't understand.

I realize that everybody goes about things their own way; I'm not looking for a "right" way to make music, more just interested in peeking into people's brains while I try to figure out my own way of doing it.

The kinds of things I'm interested in (not an exhaustive list!):

  1. How do you start? With the main melody? With a beat? Chords? Something else? Do you always start the same way or does it depend? On what?
  2. Do you create your music from scratch directly in the PICO-8 music editor or do you use something else, like a DAW and then "translate" the music over into PICO-8?
  3. Do you have particular modes, chords, or whatever that you use regularly to get that characteristic video game-y feel? Any that you avoid?
  4. Any other tips/tricks you think might be useful.

I'm interested in hearing from anybody who feels like sharing whether you have a formal music background, are self-taught or just throw a bunch of random notes together until something sounds good.

I myself have zero formal music background and was mostly never really "got" music. Don't get me wrong, I like music well enough but it's always just kind of been a thing in the background. I might know all the words to a song but not what the song is called or who performs it. Fairly recently I started playing around with LMMS and found that, in fact, making music tickles my dopamine receptors and is pretty addictive. So I've been deep diving and learning all I can.

I can do individual things, like write a melody or a chord progression, etc. but the big picture where it all comes together eludes me. Ultimately the solution to that is to learn more and make/practice (much) more but, in the meantime, let me know how you do it!

P#101096 2021-11-30 18:52

:: Unfold ::

I didn't know much about compression algorithms when I started looking into this stuff and, in truth, I still know very little. So this may all be old-hat to a lot of people but hopefully somebody finds it interesting.

What am I compressing and why do I want to compress it?

For the last month or so I've been playing around with Signed Distance Fields (SDFs) first by making some simple pictures with them and using them to handle collsiion detection and then using them as the basis of a procedural morphing animation.

I'm interested in SDFs for a few reasons: I'm not much of an artist but I do like math. If I can substitute math for art in certain situations, that potentially works to my benefit. Also, since SDFs can be used to handle collision detection and they can be updated on the fly with boolean-like operators—union, intersection and difference—they seem like they could be a good choice for modeling level geomoetry and, in particular, destructible level geometry. But mostly I just like playing with them.

In general, you create a function which returns the minimum distance from any point on screen to the surface of whatever object you're modeling and then use that distance to determine pixel colour, or detect collisions, or whatever. But calling functions, especially complex functions as SDFs tend to be, is really slow, especially if you're doing it for every single pixel on screen. To get the animations to actually animate at a reasonable speed I had to pre-calculate all the distances and store them in an array so distance checks became table look-ups. Much faster.

Creating those functions and generating those arrays required a fairly large number of tokens though. So I've been learning about compression algorithms to store those arrays directly and use a, hopefully, smaller number of tokens to decompress them.

To compress, or not to compress

Like most things, it's a trade-off: for a multi-cart system you can probably fit a decent amount of SDF data per cart; for single carts, it's almost certainly not worth it.

SDF data is big. Not as big as I had originally thought but still pretty big. Even though I was ultimately able to get quite good compression ratios we're still talking about thousands of characters worth of binary data per screen of data stored. With a fixed limit of 65535 characters, that adds up fast. In fact, as I'll discuss later, it actually adds up even faster than you'd think. Each compressed SDF only requires three tokens but saving all the tokens in the world doesn't do you any good if you don't have any characters left to use them.

Test data

I mostly used the SDFs from the animation linked above as my compression test data. Here's, sort of, what they look like as distance fields.

Left-to-right, top-to-bottom: Square, Repeated triangles, Repeated circles, Repeated squares, Star, Rotated Star, Line, and Palm tree.

It's worth noting, again, that I'm storing the actual distance data itself and not these images speciifically. The images just give a sense for how the distance fields change and how simple or complex they are. An advantage of working with distance fields in that you can use the same data in multiple ways. Here's a quick little cart which demonstates the idea:

Cart #geyukukaha-0 | 2021-11-08 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
3

Press 'x' to cycle through the different options. It's the same data in all cases, just being rendered differently.

How big is an SDF anyway?

At first I thought I might have to store fractional values so I'd need 32 bits per pixel. But no. In reality, at least how I'm using them so far, I'm working with integer distances on a single screen. The farthest away something can possibly be on a 128x128 display is about 180 or so along the diagonal: 8 bits is plenty.

Eight bits is definitely an improvement over 32 but still, that's one byte of data per pixel or 16384 bytes per screen of SDF data. At that size, a direct encoding of four SDFs would bust the PICO-8 character limit. The animation linked above uses eight SDFs.

So that number, 16384 bytes, is the base/uncompressed size for all my test data.

Compression algorithms

I tried a variety of algorithms both individually and in combination. These are the main ones.

Run length encoding (RLE)

RLE compresses by replacing a run of identical distances with a single instance of that distance and a number representing how many times it occurs before changing.

It was my assumption that RLE would be a bad choice for SDFs because, although some have long runs of repeated distances, most distances change with every pixel. If your run length is always one then instead of storing one integer per pixel, you're storing two.

Even so, I figured I'd test my assumptions by actually trying it and, sure enough, RLE on its own makes distance data larger, not smaller.

Huffman Coding

A Huffman coding encodes each unique distance with a different binary representation. Not all distances are represented with the same number of bits and the encoding is built in such a way that values which occur often use fewer bits than values which occur more rarely.

On its own, Huffman coding gave similar levels of compression as the LZW algorithm below.

Lempel-Ziv-Welch (LZW) compression

LZW is sort of, but not really, similar to RLE. It doesn't look for runs of identical distances but instead looks for sequences which it has seen before. When it finds one it inserts a reference to that sequence, essentially saying, "take that thing over there and put it over here as well."

Vector Distance Transform (VDT)

Once I thought to search for distance field specific compression algorithms, I found this paper describing VDT and it's the basis for the approach I decided to take so I'll describe it in a little more detail.

Rather than assigning a distance to each pixel, VDT assigns a vector to each pixel. The vector indicates which other (previously calculated) pixels, if any, can be used to calculate the distance for the current pixel. If we calculate pixels left-to-right and top-to-bottom then there are four possible vectors: the pixels directly above the current pixel, the pixels directly to the left of the current pixel, the pixels diagonally up and to the left of the current pixel, and the null vector indicating that the current distance can't be calculated based on previous pixels.

Since there are four possible vectors, each vector can be represented by two bits and the entire array of vectors takes up a total of 4096 bytes. Each null vector indicates a distance that we can't calculate and have to store directly, adding an additional 8 bits each, while every non-null vector is a distance that can be entirely eliminated from our data for a net savings of 6 bits each.

VDT on its own can reduce the size of an SDF fairly dramatically. But a nice feature of VDT is that, once the distance prediction step is taken, the vector data and remaining distance data can be further compressed using other methods. For instance, although RLE doesn't do so well with raw SDF data, it does a great job on the resulting vector data.

I tried two approaches and they gave very comparable results. Both start by doing the vector distance transformation.

The first approach then applied the RLE algorithm to the vector data and a Huffman coding to the distance data, finally combining the result into a single binary string.

The second approach starts by combining the vector and distance data into a single binary string and then running the LZW algorithm on that string to compress it further.

Below is a summary of the results I got via various methods. The VDT+LZW columns could just as well be VDT+RLE+Huffman since the results were very similar.

Lossless vs Lossy

I suspect that the palm tree SDF compresses so poorly because it contains a bunch of non-linear transformations: sines, cosines, exponentials, etc. which means the distance field isn't "well behaved" and, therefore, difficult to predict.

The VDT algorithm is lossless by default—it only removes a distance which can be predicted exactly—but is easily modified to be lossy. I wanted to see if I could get the palm tree SDF down to a more reasonable size without degrading the quality too badly. Spoiler alert: not really. It's easier to see when rendered as an image:

The first image is the lossless version as listed in the table above. The distortions in the second could be acceptable in some situations but still only gives a 74% compression ratio with a maximum squared error of 5. The last image, which looks like Thanos had a personal vendetta against trees, has a max squared error of 10 and still only compresses down to about 60% or a little under 10000 characters!

Compressing less to compress more

One particularly interesting discovery I made was that—even though the VDT+LZW combination gives the best compression on average—the best way to fit more SDFs per cart was to use VDT only. Why should that be the case? VDT by itself gives the worst compression of those listed. How is that better?

I figured this one out by accident when I copied the binary strings, except for the palm tree, into a cart, ran INFO and saw this:

The raw character count and the compressed character count are nearly the same. Which actually makes sense: I've compressed the data significantly so whatever algorithm PICO-8 is using to compress code isn't able to squeeze much more out of it. That particular screen shot is when using VDT+LZW but the same thing happens when using LZW only, Huffman coding only, and VDT+RLE+Huffman coding.

That got me thinking: PICO-8's compression is probably better than mine. So what if I only used VDT and let PICO-8 compress it the rest of the way for me? Here are the same seven SDFs with only VDT encoding.

It uses up a lot more of the raw character count but the compressed character count is much lower and there's still room to spare!

Edit: I forgot to actually post the compression/decompression code anywhere so here's a cart demonstrating it. The code is also on github

Cart #sdf_compression_demo-0 | 2021-11-10 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
3

P#99798 2021-11-08 18:19 ( Edited 2021-11-10 11:22)

:: Unfold ::

Cart #demo_realtime_sdf-0 | 2021-10-23 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
10

The final image is an adaptation of the image from the Principles of painting with math tutorial by Inigo Quilez.

I'm using fixed time-steps and the animation is intentionally a bit slow so "real time" might be fudging the truth a bit but I thought I wouldn't be able to animate these things at all and it turns out I was wrong. I just needed to trade off time for space in time honoured fashion. That's why there's a "loading screen" at the start: most of the time intensive calculations are being done up front. I used the SDF utility I posted the other day—not strictly necessary of course, you can just write the functions directly—to create all the signed distance functions used for the animations and then converted them to arrays by pre-calculating all the distances and turning distance checks into a simple table lookup.

This animation uses 8 such tables and takes up about 661kB (including all the code, other variables, etc.) so fits fairly comfortably in the 2MB of available lua memory.

This is mostly a proof of concept thing so the code isn't super well documented or elegant and I'm kind of just manually stringing together all the individual animations. Improvements could certainly be made but overall I'm pretty happy with how it came out.

Ideally I'd like to be able to generate the SDFs and convert them to tables from a separate cart (or another language/external tool/whatever) and export the data so it could be used in other carts. I don't know much about image/data compression algorithms so if anyone can point me towards one that might be suitable it would be greatly appreciated.

  1. The data is a 1D array of distances—which can be fractional—so each array stores a full 32-bits per pixel for every pixel on the PICO8 display. It's not image data as such so there's no requirement for it to be viewable as an image, though that might be neat.
  2. I'm not too worried about how long it takes to compress the data but fast decompression with a low-ish token count would be great.
  3. By the same token, I don't necessarily need optimal compression. Good enough is...good enough.

I thought about run length encoding but I don't think distance fields are good candidates for that in general since many rows change value every pixel.

Anyway, I hope you enjoy it. Comments, criticism, suggestions all welcome!

P#99070 2021-10-23 17:33

:: Unfold ::

PICO-8 has been my obsession for the last few months but math is my always obsession and lately I've been having a lot of fun playing around with various aspects of geometry so when I hit a weird bug in my other project I decided to take a break and shift gears a bit. So I threw together a little interface for working with 2D signed distance fields/functions.

It's fairly hefty at a little over 500 tokens so probably not super useful generally speaking but pretty fun to play with, if I do say so myself.

Here are a couple series of images I've created so far. In each, all of the images are made from the same SDF and just messing with how colours are assigned based on the distance.

That last one is my attempt at a ripply/watery reflection which, I think, didn't come out too badly.

And some hearts.

If you're not familiar with them, SDFs are frequently used with a ray marching technique to render 3D scenes as a faster alternative to ray tracing. (See Inigo Quilez's work for some stunning examples of this in action. Or basically anything on shadertoy.) Anyway, ray marching is basically just a collision detection algorithm so, apart from making pretty pictures, an SDF can also double as actual level geometry.

In the cart below I took the same SDF that was used to generate the image and used it to turn the pool/fountain thing into a collide-able part of the scenery. (In fairness, I'm not actually using ray marching here I'm really just checking for when the distance goes from positive to negative. But you could do better/smarter things if you really wanted to.)

Cart #punujehibi-0 | 2021-10-18 | Code ▽ | Embed ▽ | No License
1

In this one I just threw together a bunch of random geometry. Move around with the arrow keys and press Z to cycle which distance field(s) are visible: none, interior, exterior, or both.

Cart #ragahegaro-0 | 2021-10-18 | Code ▽ | Embed ▽ | No License
1

P#98824 2021-10-18 20:56 ( Edited 2021-10-18 20:56)

View Older Posts
Follow Lexaloffle:          
Generated 2023-02-01 09:55:12 | 0.109s | Q:78