Log In  

Cart #notita_0-4 | 2021-12-27 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
12

A PICO-8 version of a "falling sand" materials simulator, including sand, water, stone, wood, fire, acid, gunpowder and smoke!

Controls:

  • Left mouse button / x: Paint material
  • Right mouse button / o: Clear materials
  • Click the palette on left to select material to paint
  • Slider on bottom left sets tool size

This is based on bits I remember from a talk given by the creator of the game "Noita," and the clever types of materials and behaviors from that game.

To make this run acceptably in pico-8 it only simulates in portion of the screen size. Positions and materials are represented by a pico-8 color in a Lua table of fixed positions. Materials behavior is in update60() and should be easy to hack for fun. _draw() does a little bit to make flames flicker. Since the world simulates bottom-to-top, rising materials like smoke get carried along with the update and move very fast because they 'ride' the update direction.

Originally I did some experiments with 1-d tables, and also using pget/pset but didn't get good performance, so I settled on timeslicing to update bottom-to-top about 10 rows per update60(). (An artifact of this is banding in rising smoke.) The top bits in each color could probably be used to store something like particle lifespans (for burning, etc)and keep memory tight, but I opted to rely on rnd() and color-based states for things like fire instead. Using bitvectors could probably improve performance of the if..else trees for material update behavior. Somebody savvy in pico-8's particulars with cpu operations could probably optimize this far better, perhaps with direct peek/poke to vram!

Revisions:

Version 1.4:

  • Added Gunpowder and Acid
  • Refined water behavior
  • Tool size now goes down to 1

Version 1.3:

  • Optimized Erase tool to not burn CPU opcodes
  • Holding Erase button now overrides Add button (Allowing Erasing on touch screens)
  • Other minor optimizations

Version 1.2:

  • Many PICO-8 opcode optimizations (Thanks to @luchak for suggestions!)
  • Expanded the simulation area by 11 columns thanks to savings
  • Cleaned up look/feel, starts with blank slate

Version 1.1:

  • Joypad and mouse both work in the same build
  • Tool size shown next to slider
  • Momentary instructions shown
P#103361 2021-12-22 20:49 ( Edited 2021-12-27 05:14)

Needs to have a brush size. Current brush is a little large for fine-tune examination.

P#103365 2021-12-22 22:47

Thanks for giving it a go :) The bar on the lower left acts as a slider control for brush size, though it's not obvious. The number on the upper left is the current 'radius' -- maybe that should be printed below rather than above.

Update: I've clarified the controls and uploaded some vids in the first post now.

P#103373 2021-12-23 00:12 ( Edited 2021-12-23 00:41)

Hey, this is fun! Definitely a neat little toy, and I always love small physics-flavored sims. Two quick thoughts:

  • There are a lot of double property accesses in what look to be pretty tight loops, to the extent where I wonder if most of the CPU cycles that you're getting charged might be for property lookups. This might be faster if you store the grid in a strided 1D array and cache some grid values in locals? You can still do fast Y offsets with +/- width. Also consider storing the grid in a local in update() if it's not already.
  • Minor: you could probably get rid of some special-casing for edge effects with a read-only stone border surrounding the frame.
P#103415 2021-12-23 19:40 ( Edited 2021-12-23 21:31)

Thanks, luchak, I'm a fan of your physics and sound carts too :)

Your suggestions were very useful. I'd tried the 1D array approach before but was flying blind about relative costs of operations (and hadn't thought of the clever +-width approach.) Fortunately I rediscovered this:
https://pico-8.fandom.com/wiki/CPU

You're spot-on about using local variables, since globals cost 2 pico-8 cpu ops per access instead of 0 for a local. I saw huge immediate savings simply by assigning a local to the global 2D table. Then, further gains were had by caching the first lookup into the 2D table when possible. (This could be made better by tracking tbl[y][x] instead of [x][y] I think.) Otherwise, I opted to stay with the 2D table for simplicity since a table access is 2 ops and summing an offset would incur similar cost.

I used lookup tables instead of multiple conditional checks for several if() statements, since it costs only 2 ops and tables return nil for missing elements, which acts like false in this context.

The stone idea is elegant too, I did that and cut out tons of max() and min() checks.

Thanks to all this the playfield is a bit wider :)

P#103451 2021-12-24 03:24

Gunpowder & Acid added!

P#103621 2021-12-27 17:04

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2024-03-28 23:24:14 | 0.018s | Q:25