jasondelaat [Lexaloffle Blog Feed]https://www.lexaloffle.com/bbs/?uid=54967 Text adventures on Pico-8. For some reason. <p>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.</p> <p>If you're familiar with Text Adventures (or Interactive Fiction) you may know that one of&mdash;maybe <em>the</em>&mdash;most popular tool for creating them has been the <a href="https://en.wikipedia.org/wiki/Inform">Inform</a> language. The current version is <a href="https://ganelson.github.io/inform-website/">Inform 7</a> but <em>waaaaay</em> back when I was first learning, it is was <a href="https://github.com/DavidKinder/Inform6">Inform 6.</a></p> <p>Anyway, I decided to throw together a quick little <a href="https://github.com/jasondelaat/pico8-tools/tree/release/pico-informant">IF authoring API loosely based on Inform 6.</a> It is by no means complete or particularly advanced. Basically, I followed the first tutorial for the game <em>Heidi</em> from <a href="https://ifarchive.org/if-archive/infocom/compilers/inform6/manuals/IBG.pdf">The Inform Beginner's Guide</a> and implemented just enough to make it work. But work it does! Mostly. I think...</p> <p>The command parser is <em>extremely</em> simple so don't expect too much from it. All commands should be in the form:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>verb [noun] [noun]</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Where the nouns are optional depending on what you're trying to do. So <code>east</code> or <code>e</code> to walk to the east; <code>take object</code> to pick up an object; and <code>put object target</code> 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.</p> <p> <table><tr><td> <a href="/bbs/?pid=117587#p"> <img src="/bbs/thumbs/pico8_jd_heidi_if-3.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=117587#p"> jd_heidi_if</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=117587#p"> [Click to Play]</a> </td></tr></table> </p> <p>The API clocks in at 926 tokens while the example game itself is only 173.</p> <p>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.</p> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"></p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>--&gt;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', &quot;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.&quot;) -- rooms and objects before_cottage = object('in front of a cottage') description(&quot;you stand outside a cottage.\nthe forest stretches east.\n&quot;) has(light) forest = object('deep in the forest') description(&quot;through the dense foliage you\nglimpse a building to the west.\na track heads to the northeast.&quot;) has(light) bird = object('baby bird', forest) description(&quot;too young to fly, the nestling\ntweets helplessly.&quot;) name('baby', 'bird', 'nestling') clearing = object('a forest clearing') description(&quot;a tall sycamore stands in the\nmiddle of this clearing. the\npath winds southwest through the\ntrees.&quot;) has(light) nest = object(&quot;bird's nest&quot;, clearing) description(&quot;the nest is carefully woven of \ntwigs and moss.\n &quot;) name(&quot;bird's&quot;, 'nest', 'twigs', 'moss') has(container|open) tree = object('tall sycamore tree', clearing) description(&quot;standing proud in the middle of \n the clearing, the stout tree \n looks easy to climb.\n &quot;) name('tall', 'sycamore', 'tree', 'stout', 'proud') has(scenery) top_of_tree = object('at the top of the tree') description(&quot;you cling precariously to the \ntrunk.&quot;) 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(&quot;it's flat enough to support a \nsmall object.\n &quot;) 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</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p></div></div></div></p> <h3>Walkthrough</h3> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"><br /> 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&mdash;as defined by the max_carried variable in the _init() function&mdash;but an object inside of something else counts as a single object.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>in front of a cottage -------------------------------- you stand outside a cottage. the forest stretches east. &gt; 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. &gt; take bird you take the baby bird. &gt; 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. &gt; put bird nest you put the baby bird in the bird's nest. &gt; take nest you take the bird's nest. &gt; climb at the top of the tree -------------------------------- you cling precariously to the trunk. you see a wide firm bough. &gt; put nest bough you win!</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Some of the words have multiple aliases. Instead of 'bough' you could type 'put nest branch' and that would also work.<br /> </div></div></div></p> https://www.lexaloffle.com/bbs/?tid=49378 https://www.lexaloffle.com/bbs/?tid=49378 Sun, 18 Sep 2022 10:44:36 UTC midi 122 Seashore <p> <iframe src="sfxp.php?id=54967_27" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_27"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_27.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_27"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_27" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <p>Made for the <a href="https://www.lexaloffle.com/bbs/?tid=49265">midilib custom SFX instrument project</a></p> <p>And in action:<br /> <table><tr><td> <a href="/bbs/?pid=117347#p"> <img src="/bbs/thumbs/pico8_jdmidi_122_seashore-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=117347#p"> jdmidi_122_seashore</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=117347#p"> [Click to Play]</a> </td></tr></table> </p> <h5>The animation is taken from one of my other projects. The palm tree image is adapted from the tutorial <a href="https://www.youtube.com/watch?v=0ifChJ0nJfM&amp;amp;t=1233s">The basics of Painting with Maths</a> by Inigo Quilez.</h5> https://www.lexaloffle.com/bbs/?tid=49321 https://www.lexaloffle.com/bbs/?tid=49321 Tue, 13 Sep 2022 19:06:14 UTC midi 125 Helicopter <p>I mean, who <em>doesn't</em> compose for helicopter, right?</p> <p>Made for the <a href="https://www.lexaloffle.com/bbs/?tid=49265">midilib custom SFX instrument project</a></p> <p>Credit's always nice but not required.</p> <p> <iframe src="sfxp.php?id=54967_26" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_26"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_26.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_26"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_26" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <p>Here it is in action. Please forgive the poorly and hastily drawn helicopter.<br /> <table><tr><td> <a href="/bbs/?pid=117345#p"> <img src="/bbs/thumbs/pico8_jdmidi_125_helicopter-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=117345#p"> jdmidi_125_helicopter</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=117345#p"> [Click to Play]</a> </td></tr></table> </p> https://www.lexaloffle.com/bbs/?tid=49320 https://www.lexaloffle.com/bbs/?tid=49320 Tue, 13 Sep 2022 10:12:06 UTC The Animation of Final Fantasy <p>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.</p> <p><a href="https://www.youtube.com/playlist?list=PLugegG07di39Hn8m_Q5WwqZJsvYiThnO7">The Animation of Final Fantasy</a></p> <p>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.</p> <p>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&mdash;especially in FF-II&mdash;how they were able to use those assets to tell a story and convey at least some <em>very</em> basic sense of character.</p> https://www.lexaloffle.com/bbs/?tid=49301 https://www.lexaloffle.com/bbs/?tid=49301 Mon, 12 Sep 2022 09:53:39 UTC PICO Composer <h1>PICO Composer v0.3</h1> <h3>Changes</h3> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"><br /> 2022-06-15:</p> <ul> <li>Set default tempo to 120 bpm</li> <li>Set default time signature to 4/4</li> <li>Implemented exporter and importer code</li> </ul> <p>2022-06-06:</p> <ul> <li>Fixed display code which wasn't properly accounting for sharps causing notes to eventually be drawn offscreen.<br /> </div></div></div></li> </ul> <p> <table><tr><td> <a href="/bbs/?pid=111852#p"> <img src="/bbs/thumbs/pico8_picomposer-5.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=111852#p"> picomposer</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=111852#p"> [Click to Play]</a> </td></tr></table> </p> <h2>PICO Composer main menu</h2> <p>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 <code>enter</code> 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.</p> <p>When creating a new album the album name will be the name of the file&mdash;no need to specify .p8 extension&mdash;where your work will be saved. At the moment each &quot;album&quot; 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.</p> <h2>Controls:</h2> <p>PICO Composer is intended to be run locally with a full keyboard and will save your work to a separate .p8 file.</p> <img style="" border=0 src="/media/54967/pico composer v0_3 controls.png" alt="" /> <h2>Demo Song/Album</h2> <p>Download <a href="https://www.lexaloffle.com/bbs/files/54967/song6.p8">this cart</a> and save it in the same directory as <code>picomposer.p8.png</code> Load and run picomposer and when prompted select <code>load album</code>, type <code>song6</code> and press enter. It should load and after a couple of seconds you'll see the last few notes of the song. Press <code>space</code> to play the song from the beginning.</p> <p>When you export an album (press <code>e</code>) a new file <code>album_name.music.p8</code> 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 <code>load_music()</code> and <code>play()</code> functions.</p> <p> <table><tr><td> <a href="/bbs/?pid=111852#p"> <img src="/bbs/thumbs/pico8_picomposer_import-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=111852#p"> picomposer_import</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=111852#p"> [Click to Play]</a> </td></tr></table> </p> <p>(Note: The song data for this cart is in the spritesheet and it's &quot;importing&quot; from itself into upper memory. Usually you'll be importing from the generated .music.p8 file but I wanted a self contained example.)</p> <p>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.</p> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"><br /> Song 6 refers to my <a href="https://www.lexaloffle.com/bbs/?tid=47406">Tunes!</a> thread. This demo song is the same song as the 6th (currently last) song I've posted there.</p> <p>As a point of comparison: By my calculation &quot;Song 6&quot; 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&mdash;I just had to enter every single note&mdash;that gives me hope that I'll be able to keep song sizes down.<br /> </div></div></div></p> <h2>Future Development/Feature Wishlist</h2> <ul> <li>Select/edit note(s)</li> <li>Allow all the pico-8 instruments and custom instruments</li> <li>Multiple songs per album</li> <li>Dynamics: volume pp, p, mp, mf, f, ff, fff </li> <li>Allow up to 4 voices and ability to switch around between them </li> <li>Clefs. As is the staff is in treble clef. Adding, at least, bass clef would be useful </li> <li>Bar lines </li> <li>crescendo, decrecendo </li> <li>Repeat symbols and voltas </li> <li>Effects/articulations (tremolo, etc.) </li> <li>Maybe a mode specifically for notating drum parts </li> <li>Possibly the ability to convert to/from the pico-8 tracker format </li> <li>Joining notes when possible (connected 8th notes, for instance) </li> <li>Ties and slurs </li> <li>key signatures </li> <li>Draw leger lines </li> <li>Start screen with nicer menus</li> <li>Reduce importer code size.</li> </ul> <p>Proof of concept version:<br /> <div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"><br /> 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.</p> <p>Here's a quick little gif demo. No sound obviously but imagine each note playing as it's entered and also on playback:</p> <img style="" border=0 src="/media/54967/pi_composer p8_0.gif" alt="" /> <p>Try it out:<br /> <table><tr><td> <a href="/bbs/?pid=111852#p"> <img src="/bbs/thumbs/pico8_picomposer-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=111852#p"> picomposer</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=111852#p"> [Click to Play]</a> </td></tr></table> </p> <h2>Controls and (current lack of) Features</h2> <p>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. </p> <p>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.</p> <h3>Note Entry</h3> <h4>Note Values</h4> <p>The top right corner shows you what note value will be inserted next. You can change this with number keys 1-5:</p> <ul> <li>1 - whole notes</li> <li>2 - half notes</li> <li>3 - quarter notes</li> <li>4 - eighth notes</li> <li>5 - sixteenth notes</li> </ul> <h4>Pitches</h4> <p>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.</p> <p>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&mdash;or something else&mdash;to change which octave you're in.</p> <p>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.</p> <h4>Playback</h4> <p>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.</p> <h3>Future Development</h3> <p>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.</p> <ul> <li>Save/load songs to memory and/or another cart</li> <li>Octaves for a full range of notes as mentioned above</li> <li>Clefs. As is the staff is in treble clef. Adding, at least, bass clef would be useful</li> <li>Actually allow you to enter tempo and time signature</li> <li>Bar lines</li> <li>Allow up to 4 voices and ability to switch around between them</li> <li>Allow all the pico-8 instruments and custom instruments</li> <li>Dynamics (p, f, mf, pp, crescendos, etc.)</li> <li>Repeat symbols and voltas</li> <li>Effects/articulations (tremolo, etc.)</li> <li>Maybe a mode specifically for notating drum parts</li> <li>A way to load and play the songs in carts</li> <li>Possibly the ability to convert to/from the pico-8 tracker format</li> <li>Joining notes when possible (connected 8th notes, for instance)</li> <li>Ties and slurs</li> <li>Add rests</li> <li>Select/edit/delete note(s)</li> <li>key signatures</li> </ul> <p>That's what I've got off the top of my head. Suggestions welcome.<br /> </div></div></div></p> https://www.lexaloffle.com/bbs/?tid=47819 https://www.lexaloffle.com/bbs/?tid=47819 Mon, 16 May 2022 10:44:52 UTC Function currying for fun and....fun <p>I may have a non-standard definition of fun...</p> <p><em><strong>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!</strong></em></p> <p>This is basically just a function which allows you to create curried functions&mdash;which I attempt to explain below if you're unfamiliar with them&mdash;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.</p> <p>Anyway, <a href="https://github.com/jasondelaat/pico8-tools/tree/release/curry">here it is on github</a> or you can copy/paste the code from the example.</p> <h3>Example</h3> <p>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&mdash;or even good&mdash;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.</p> <p> <table><tr><td> <a href="/bbs/?pid=111095#p"> <img src="/bbs/thumbs/pico8_curry_demo-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=111095#p"> curry_demo</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=111095#p"> [Click to Play]</a> </td></tr></table> </p> <h3>What is function currying?</h3> <p>If you've never encountered curried functions before they can seem a bit weird at first but they're actually pretty simple.</p> <p>Function currying &mdash; which gets its name from mathematician <a href="https://en.wikipedia.org/wiki/Haskell_Curry">Haskell Curry</a> for whom the Haskell programming language is also named &mdash; 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:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> function sum3(x, y, z) return x + y + z end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>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:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> function sum3_curried(x) return function(y) return function(z) return x + y + z end end end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The <code>curry</code> function basically does this for you so instead of having to write a bunch of nested functions manually you could just do this:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> sum3_curried = curry(3, sum3)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Or this without having to define <code>sum3</code> first:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> sum3_curried = curry( 3, function(x, y, z) return x + y + z end )</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Which makes it easier to see what the function is doing without having to wade through multiple levels of nested functions.</p> <h3>And I would want to do that because&hellip;?</h3> <p>Curried functions can be useful in a bunch of situations but they all basically come down to one thing: partial application.</p> <p>With the <code>sum3</code> function you have to pass all the arguments at once or else you'll get an error. With <code>sum3_curried</code> 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.</p> <h3>But isn't <code>sum3_curried(1)(2)</code> kind of ugly and annoying to write?</h3> <p>It sure is!</p> <p>The <code>curry</code> function doesn't actually construct a bunch of nested functions. Instead, the function returned by <code>curry</code> 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.</p> <p>These are all valid ways of calling the versions of <code>sum3_curried</code> created with <code>curry</code>:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> sum3_curried(1, 2, 3) sum3_curried(1)(2, 3) sum3_curried(1, 2)(3) sum3_curried(1)(2)(3)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> https://www.lexaloffle.com/bbs/?tid=47601 https://www.lexaloffle.com/bbs/?tid=47601 Tue, 03 May 2022 21:16:33 UTC Tunes! <p>Just creating a thread to collect music that I write. Mostly just for myself&mdash;to have things I like all in one place which I'll update periodically as I write new stuff&mdash;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.</p> <p>With that in mind here's my first entry. A jaunty little tune I threw together this morning.</p> <h3>2022-04-16:</h3> <p>Song 1: Jaunty Tune in Eb Major<br /> <iframe src="sfxp.php?id=54967_17" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_17"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_17.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_17"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_17" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <p>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.</p> <p>Song 2:<br /> <iframe src="sfxp.php?id=54967_18" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_18"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_18.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_18"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_18" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <h3>2022-04-17:</h3> <p>A Less Jaunty Tune in Eb Minor<br /> 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.</p> <p>Song 3:<br /> <iframe src="sfxp.php?id=54967_19" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_19"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_19.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_19"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_19" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <h3>2022-05-09</h3> <p>Song 4:<br /> <iframe src="sfxp.php?id=54967_20" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_20"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_20.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_20"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_20" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <p>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.</p> <p>Song 5:<br /> <iframe src="sfxp.php?id=54967_21" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_21"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_21.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_21"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_21" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <p>Another older one because apparently today is my day to copy stuff over into the tracker.<br /> Song 6:<br /> <iframe src="sfxp.php?id=54967_22" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_22"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_22.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_22"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_22" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <h3>2022-06-22</h3> <p>Song 7: Playing around with 4 part SATB writing.<br /> <iframe src="sfxp.php?id=54967_24" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_24"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_24.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_24"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_24" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> https://www.lexaloffle.com/bbs/?tid=47406 https://www.lexaloffle.com/bbs/?tid=47406 Sat, 16 Apr 2022 17:26:57 UTC Song WIP <p> <iframe src="sfxp.php?id=54967_15" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_15"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_15.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_15"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_15" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <p>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.</p> <p>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.</p> <p>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 (<a href="https://www.lexaloffle.com/bbs/?tid=45515">Tell me about how you compose music</a>) to discuss making music in general and got a bunch of great replies. Thanks in particular to <a href="https://www.lexaloffle.com/bbs/?uid=40166"> @packbat</a> 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> https://www.lexaloffle.com/bbs/?tid=46884 https://www.lexaloffle.com/bbs/?tid=46884 Mon, 07 Mar 2022 19:51:24 UTC Tell me about how you compose music <p>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&mdash;I'm fine with that&mdash;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.</p> <p>I realize that everybody goes about things their own way; I'm not looking for a &quot;right&quot; 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.</p> <p>The kinds of things I'm interested in (not an exhaustive list!):</p> <ol> <li>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?</li> <li>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 &quot;translate&quot; the music over into PICO-8?</li> <li>Do you have particular modes, chords, or whatever that you use regularly to get that characteristic video game-y feel? Any that you avoid?</li> <li>Any other tips/tricks you think might be useful.</li> </ol> <p>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.</p> <p>I myself have zero formal music background and was mostly never really &quot;got&quot; 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 <a href="https://lmms.io/">LMMS</a> 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.</p> <p>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> https://www.lexaloffle.com/bbs/?tid=45515 https://www.lexaloffle.com/bbs/?tid=45515 Tue, 30 Nov 2021 18:52:43 UTC Adventures in data compression <p>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.</p> <h2>What am I compressing and why do I want to compress it?</h2> <p>For the last month or so I've been playing around with Signed Distance Fields (SDFs) first by making some <a href="https://www.lexaloffle.com/bbs/?tid=45036">simple pictures with them and using them to handle collsiion detection</a> and then using them as the basis of a <a href="https://www.lexaloffle.com/bbs/?tid=45116">procedural morphing animation.</a> </p> <p>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&mdash;union, intersection and difference&mdash;they seem like they could be a good choice for modeling level geomoetry and, in particular, <em>destructible</em> level geometry. But mostly I just like playing with them.</p> <p>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 <em>really</em> 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. <em>Much</em> faster.</p> <p>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.</p> <h2>To compress, or not to compress</h2> <p>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.</p> <p>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 <em>per screen</em> 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.</p> <h2>Test data</h2> <p>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. </p> <img style="" border=0 src="/media/54967/fields.png" alt="" /> <p>Left-to-right, top-to-bottom: Square, Repeated triangles, Repeated circles, Repeated squares, Star, Rotated Star, Line, and Palm tree.</p> <p>It's worth noting, again, that I'm storing the actual distance data itself and <em>not</em> 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:</p> <p> <table><tr><td> <a href="/bbs/?pid=99798#p"> <img src="/bbs/thumbs/pico8_geyukukaha-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=99798#p"> geyukukaha</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=99798#p"> [Click to Play]</a> </td></tr></table> </p> <p>Press 'x' to cycle through the different options. It's the same data in all cases, just being rendered differently. </p> <h3>How big is an SDF anyway?</h3> <p>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.</p> <p>Eight bits is definitely an improvement over 32 but still, that's one byte of data per pixel or 16384 bytes <em>per screen</em> of SDF data. At that size, a direct encoding of four SDFs would bust the PICO-8 character limit. The animation linked above uses <em>eight</em> SDFs. </p> <p>So that number, 16384 bytes, is the base/uncompressed size for all my test data.</p> <h2>Compression algorithms</h2> <p>I tried a variety of algorithms both individually and in combination. These are the main ones.</p> <h3>Run length encoding (RLE)</h3> <p><a href="https://en.wikipedia.org/wiki/Run-length_encoding">RLE</a> 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. </p> <p>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.</p> <p>Even so, I figured I'd test my assumptions by actually trying it and, sure enough, RLE on its own makes distance data <em>larger,</em> not smaller. </p> <h3>Huffman Coding</h3> <p>A <a href="https://en.wikipedia.org/wiki/Huffman_coding">Huffman coding</a> 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.</p> <p>On its own, Huffman coding gave similar levels of compression as the LZW algorithm below.</p> <h3>Lempel-Ziv-Welch (LZW) compression</h3> <p><a href="https://en.wikipedia.org/wiki/Lempel%25E2%2580%2593Ziv%25E2%2580%2593Welch">LZW</a> is sort of, but not really, similar to RLE. It doesn't look for runs of identical distances but instead looks for <em>sequences</em> which it has seen before. When it finds one it inserts a reference to that sequence, essentially saying, &quot;take that thing over there and put it over here as well.&quot;</p> <h3>Vector Distance Transform (VDT)</h3> <p>Once I thought to search for distance field specific compression algorithms, I found <a href="https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.10.4624&amp;amp;rep=rep1&amp;amp;type=pdf">this paper describing VDT</a> and it's the basis for the approach I decided to take so I'll describe it in a little more detail.</p> <p>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.</p> <p>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.</p> <p>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. </p> <p>I tried two approaches and they gave very comparable results. Both start by doing the vector distance transformation. </p> <p>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.</p> <p>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.</p> <p>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.</p> <img style="" border=0 src="/media/54967/compression-table.png" alt="" /> <p><strong>Lossless vs Lossy</strong></p> <p>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 &quot;well behaved&quot; and, therefore, difficult to predict.</p> <p>The VDT algorithm is lossless by default&mdash;it only removes a distance which can be predicted exactly&mdash;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:</p> <img style="" border=0 src="/media/54967/lossy.png" alt="" /> <p>The first image is the lossless version as listed in the table above. The distortions in the second <em>could</em> 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 <em>still</em> only compresses down to about 60% or a little under 10000 characters!</p> <h2>Compressing less to compress more</h2> <p>One particularly interesting discovery I made was that&mdash;even though the VDT+LZW combination gives the best compression on average&mdash;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 <em>worst</em> compression of those listed. How is that <em>better?</em></p> <p>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:</p> <img style="" border=0 src="/media/54967/vdt_lzw.png" alt="" /> <p>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.</p> <p>That got me thinking: PICO-8's compression is probably better than mine. So what if I <em>only</em> 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.</p> <img style="" border=0 src="/media/54967/vdt_only.png" alt="" /> <p>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!</p> <p>Edit: I forgot to actually post the compression/decompression code anywhere so here's a cart demonstrating it. The code is also on <a href="https://github.com/jasondelaat/pico8-tools/tree/release/sdf">github</a></p> <p> <table><tr><td> <a href="/bbs/?pid=99798#p"> <img src="/bbs/thumbs/pico8_sdf_compression_demo-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=99798#p"> sdf_compression_demo</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=99798#p"> [Click to Play]</a> </td></tr></table> </p> https://www.lexaloffle.com/bbs/?tid=45300 https://www.lexaloffle.com/bbs/?tid=45300 Mon, 08 Nov 2021 18:19:21 UTC Demo: Real(-ish) time SDF morphing animations <p> <table><tr><td> <a href="/bbs/?pid=99070#p"> <img src="/bbs/thumbs/pico8_demo_realtime_sdf-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=99070#p"> demo_realtime_sdf</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=99070#p"> [Click to Play]</a> </td></tr></table> </p> <p>The final image is an adaptation of the image from the <a href="https://www.youtube.com/watch?v=0ifChJ0nJfM&amp;amp;t=1155s">Principles of painting with math</a> tutorial by <a href="https://iquilezles.org/">Inigo Quilez.</a></p> <p>I'm using fixed time-steps and the animation is intentionally a bit slow so &quot;real time&quot; 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 &quot;loading screen&quot; at the start: most of the time intensive calculations are being done up front. I used the SDF utility <a href="https://www.lexaloffle.com/bbs/?tid=45036">I posted the other day</a>&mdash;not strictly necessary of course, you can just write the functions directly&mdash;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.</p> <p>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. </p> <p>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.</p> <p>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. </p> <ol> <li>The data is a 1D array of distances&mdash;which can be fractional&mdash;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 <em>as</em> an image, though that might be neat. </li> <li>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.</li> <li>By the same token, I don't necessarily need optimal compression. Good enough is...good enough.</li> </ol> <p>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.</p> <p>Anyway, I hope you enjoy it. Comments, criticism, suggestions all welcome!</p> https://www.lexaloffle.com/bbs/?tid=45116 https://www.lexaloffle.com/bbs/?tid=45116 Sat, 23 Oct 2021 17:33:56 UTC Pretty pictures and signed distance fields <p>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 <a href="https://www.lexaloffle.com/bbs/?tid=44934">other project</a> I decided to take a break and shift gears a bit. So I threw together a little interface for working with <a href="https://github.com/jasondelaat/pico8-tools/tree/release/sdf">2D signed distance fields/functions.</a></p> <p>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.</p> <p>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.</p> <img style="" border=0 src="/media/54967/moon.png" alt="" /> <p>That last one is my attempt at a ripply/watery reflection which, I think, didn't come out too badly.</p> <p>And some hearts.</p> <img style="" border=0 src="/media/54967/heartish.png" alt="" /> <p>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 <a href="https://iquilezles.org/live/index.htm">Inigo Quilez's work</a> for some stunning examples of this in action. Or basically anything on <a href="https://www.shadertoy.com/">shadertoy</a>.) 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.</p> <p>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.)</p> <p> <table><tr><td> <a href="/bbs/?pid=98824#p"> <img src="/bbs/thumbs/pico8_punujehibi-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=98824#p"> punujehibi</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=98824#p"> [Click to Play]</a> </td></tr></table> </p> <p>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.</p> <p> <table><tr><td> <a href="/bbs/?pid=98824#p"> <img src="/bbs/thumbs/pico8_ragahegaro-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=98824#p"> ragahegaro</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=98824#p"> [Click to Play]</a> </td></tr></table> </p> https://www.lexaloffle.com/bbs/?tid=45036 https://www.lexaloffle.com/bbs/?tid=45036 Mon, 18 Oct 2021 20:56:24 UTC Let there be light! <p>Building on my previous <a href="https://www.lexaloffle.com/bbs/?tid=44756">ray casting</a> toy I've been playing around with shadow casting.</p> <p> <table><tr><td> <a href="/bbs/?pid=98442#p"> <img src="/bbs/thumbs/pico8_difepoduko-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=98442#p"> difepoduko</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=98442#p"> [Click to Play]</a> </td></tr></table> </p> <p>Old (30fps):</p> <img style="" border=0 src="/media/54967/18_shadow_1.gif" alt="" /> <p>New (60fps):</p> <img style="" border=0 src="/media/54967/shadow p8_0.gif" alt="" /> <p>Most examples of this I've come across work with tiles/on a grid. This works with arbitrary line segments with the following caveat: the more line segments, the worse the performance. This example runs okay at 30fps but not so much at 60fps.</p> <p>Most of my time seems to be lost in the actual casting of rays.</p> <p>Things I plan to do but haven't done yet:</p> <ol> <li>Currently I'm casting two rays, slightly offset, towards every line segment's end-points (after removing duplicates so I'm not casting at the same point more than once.) I figure it shouldn't be too terribly difficult to modify it so a ray which hits a line's end-point is extended to the next wall instead of stopping. Then I'd only need one ray per end-point cutting my total number of rays in half.</li> <li>Culling the geometry before casting so instead of casting at everything, I only cast at everything within a visible radius. Bonus, that'll also make it easy to only cast in the direction the character is facing instead of casting in all directions.</li> </ol> <p>I do sort things a few times and I'm using bubble sort because it was easy to implement but I'm sorting pretty short lists so I don't think it's a huge time sink.</p> <p>Suggestions on how to optimize further are very welcome!</p> <p>Edit:<br /> Optimized a couple things:</p> <ol> <li>Made casting rays more efficient by in-lining the wall/ray intersection calculations.</li> <li>Made removing duplicates more efficient by using a set-like data structure.</li> <li>There are enough line segments/end-points that a more efficient search makes sense so threw together a (probably pretty bad) merge sort.</li> </ol> <p>The new version has approximately the same CPU usage at 60fps that the old version had at 30fps. I'm sure some optimization wizards out there can help me squeeze some more out of this.</p> https://www.lexaloffle.com/bbs/?tid=44934 https://www.lexaloffle.com/bbs/?tid=44934 Sat, 09 Oct 2021 20:41:12 UTC Playing around with ray casting <p>Had a video on ray casting pass by in my Youtube feed the other day and thought I'd play around with it a bit.</p> <p><img style="" border=0 src="/media/54967/raycast_3.gif" alt="" /> <img style="" border=0 src="/media/54967/raycast_2.gif" alt="" /></p> <p>There are the four outer walls and then four randomly generated interior walls. It's casting 27 rays so, when moving, that's 216 intersections calculated per frame at 60 fps. Either more walls or more rays forces it down to 30 fps so it's a bit of a performance hog (okay, a huge performance hog.) I'm sure it could be optimized somewhat, though I'm unlikely to do it.</p> <p>Edit: Okay, so apparently I lied. I realized that I could have my rays do double duty by calculating two intersection points each, one in each direction along the line. So I get effectively twice as many rays for virtually no extra cost.</p> <img style="" border=0 src="/media/54967/raycast_4.gif" alt="" /> <p> <table><tr><td> <a href="/bbs/?pid=97805#p"> <img src="/bbs/thumbs/pico8_yirijahiwe-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=97805#p"> yirijahiwe</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=97805#p"> [Click to Play]</a> </td></tr></table> </p> <p>Here's a quick little modification I made which uses the same logic to handle actual collisions with the walls. It's only casting one ray in the direction of movement so you could add a lot more walls before you notice performance issues. It's not perfect. Every once in a while the red dot will zip right through a wall. I suspect this is just because of decimal precision errors and in an actual game could be fixed by checking more than one point for collisions: the four corners of the sprite for instance. But, again, I probably won't bother.</p> <p> <table><tr><td> <a href="/bbs/?pid=97805#p"> <img src="/bbs/thumbs/pico8_tewetaripi-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=97805#p"> tewetaripi</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=97805#p"> [Click to Play]</a> </td></tr></table> </p> <p>Oh, and the video:<br /> <object width="640" height="400"><param name="movie" value="https://www.youtube.com/v/TOEi6T2mtHo&hl=en&fs=1&rel=0"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="https://www.youtube.com/v/TOEi6T2mtHo&hl=en&fs=1&rel=0" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="640" height="400"></embed></object></p> https://www.lexaloffle.com/bbs/?tid=44756 https://www.lexaloffle.com/bbs/?tid=44756 Sat, 25 Sep 2021 20:00:04 UTC Managing state machines <h1>State Machines</h1> <p>A wrote (a couple variations of) a simple state machine manager. The code is on <a href="https://github.com/jasondelaat/pico8-tools">github</a> under an <a href="https://github.com/jasondelaat/pico8-tools/blob/release/LICENSE">MIT license</a>. There are a couple demos down at the bottom of the page.</p> <ul> <li><strong>state-machines-du (107 Tokens):</strong> each state has its own draw and<br /> update methods</li> <li><strong>state-machines-st (111 Tokens):</strong> each state has setup and teardown<br /> methods which are run only when the state is entered and exited<br /> respectively</li> </ul> <h2>Usage</h2> <h3>Creating a state machine</h3> <p>To create a new state machine use the 'new' method:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> sm = state_machine:new()</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <h3>Adding states</h3> <p>After creating the machine itself you need to add states. A state<br /> consists of four things:</p> <ul> <li><strong>A unique name or identifier:</strong> Most likely a string but can be<br /> anything as long as it's unique.</li> <li><strong>A transition function:</strong> This function is called once per cycle<br /> and should return the identifier of the state the machine<br /> should switch to.</li> <li><strong>An update (or setup) function:</strong> The update function is called<br /> once per cycle and should update variables, etc. associated<br /> with the state. For state-machine-st.lua, this is instead a<br /> setup function which is only run once each time the machine<br /> enters this state.</li> <li><strong>A draw (or teardown) function:</strong> The draw function is called<br /> once per cycle and should draw everything relevant to the<br /> state. For state-machine-st.lua, this is instead a teardown<br /> function which is only run once each time the machine exits<br /> this state.</li> </ul> <p>Add a state to the machine using the 'add_state' method:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> sm:add_state( -- identifier 'a state', -- transition function function() if btnp(5) then return 'some other state' else return 'a state' end end, -- update function function() if timer then timer += 1 else timer = 0 end end, -- draw function function() print(timer) end )</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <h3>Using the state machine</h3> <ol> <li> <p>state-machine-du.lua</p> <p>Once you've created a state machine and added some states using it<br /> is simple: Set the initial state then call the update and draw<br /> methods.</p> </li> </ol> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> function _init() sm:set_state('a state') end function _update() sm:update() end function _draw() cls() sm:draw() end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The update method calls the current state's transition function<br /> and changes the current state if necessary and then calls the<br /> current state's update function. The draw method calls the current<br /> state's draw function.</p> <ol start="2"> <li> <p>state-machine-st.lua</p> <p>The setup/teardown version is basically the same except there is<br /> no draw method and the update method does a bit more work, so all<br /> you need is this:</p> </li> </ol> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> function _init() sm:set_state('state 1') end function _update() sm:update() -- whatever other update stuff you need to do. end function _draw() cls() -- whatever draw stuff you need to do. end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The update method in this version also calls the transition<br /> function. If a state change is necessary, then the current state's<br /> teardown function is called, then the current state is changed,<br /> and finally the new state's setup function is called.</p> <h2>Demos</h2> <h3>basic-sm</h3> <p>A very basic state machine with two states. Press X/V to switch from state 1 to state 2, Z/C to switch from state 2 to state 1.<br /> <table><tr><td> <a href="/bbs/?pid=95647#p"> <img src="/bbs/thumbs/pico8_basic_sm-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=95647#p"> basic_sm</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=95647#p"> [Click to Play]</a> </td></tr></table> </p> <h3>platform-tut</h3> <p>A simple platformer demo with 5 states: intro, tut_movement, tut_jump, play, and gameover.<br /> <table><tr><td> <a href="/bbs/?pid=95647#p"> <img src="/bbs/thumbs/pico8_platform_tut_sm-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=95647#p"> platform_tut_sm</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=95647#p"> [Click to Play]</a> </td></tr></table> </p> https://www.lexaloffle.com/bbs/?tid=44090 https://www.lexaloffle.com/bbs/?tid=44090 Wed, 04 Aug 2021 09:55:16 UTC 8x8 pixel art study <p>I've finally started working on an actual game. The programming is not a problem but art, on the other hand, well...there be dragons.</p> <p>I'm not an artist and it doesn't come naturally to me but pixel art, 8x8 pixel art in particular, seems much more approachable than more traditional types of art. I think because the feedback cycle is shorter and more obvious: with only 64 pixels to play with, it's pretty obvious when you put one in the wrong spot and there are a limited number of choices available for how to fix the problem. So I spent a few days poking around the sprite editor doing a bit of an &quot;art study&quot; to see what I could figure out. Comments, tips, tricks, etc. very much welcome.</p> <p>This is very much <em>not</em> a tutorial. I'm not and artist and I'm not qualified to teach anybody anything about doing art of any kind. If people find it interesting or useful, great! But mostly I'm just using this as a place to record observations for future me to think about when I'm trying to figure out what the heck my game should actually look like. I'll add the full sprite sheet at the bottom if anyone's interested in having a look at it.</p> <h2>Basic proportions</h2> <p>One of the things I remember from my many failed attempts to learn how to draw is that people are generally about 8 heads tall, which seems ideal for an 8x8 box. One for the head (duh), three for the torso, and four for the legs, with the hand hanging just below the waist. Sketching that out as a sort of measuring stick, I got this:<br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_0.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_0"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_0.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_0"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_0" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <p>Trying to make that into a front view though presented immediate problems:<br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_1.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_1"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_1.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_1"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_1" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <p>The shoulders are a bit too broad compared to the head but the main problem is the pogo-stick leg. So obviously I had to mess with the proportions a bit to get something that looked acceptable.<br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_2.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_2"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_2.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_2"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_2" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_3.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_3"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_3.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_3"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_3" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <p>The legs had to be shortened to allow for the larger head but at least now I get hair! This is, I think, probably about as close to &quot;proper&quot; human proportions as it's possible to get inside and 8x8 box.</p> <h2>Messing with proportions</h2> <p>Alright, so how does it change if I mess with the proportions further? Maybe other people will read these differently than I do but here's what I've got:</p> <ol> <li> <p>Extending the torso an additional pixel gives him a solid, football player-ish kind of feeling.<br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_4.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_4"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_4.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_4"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_4" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> </li> <li> <p>Making the legs one pixel longer, on the other hand, gives a more prim and proper, possibly slightly<br /> stuck-up vibe.<br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_5.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_5"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_5.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_5"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_5" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> </li> <li> <p>Lowering only the shoulders gives him a &quot;stylish&quot; turtle neck.<br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_6.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_6"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_6.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_6"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_6" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> </li> <li> <p>While making the head larger makes the character feel a bit more heavy-set. Especially if the torso is<br /> also extended an extra pixel making the legs shorter.<br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_7.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_7"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_7.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_7"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_7" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_8.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_8"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_8.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_8"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_8" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> </li> <li> <p>I did a couple characters at 8x16 with the same basic proportions where each pixel in the 8x8 becomes a<br /> group of 4 pixels in the 8x16 which can then be edited to keep the proportions more in-line. Didn't do<br /> many of these though<br /> <table><tr><td width=64> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_9.png" width=64 height=64> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_9"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_9.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_9"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [16x16]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_9" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> </li> <li> <p>And then, of course, with a bit of understanding of the proportions and how to balance them, I went for<br /> the big-head, small-body look. I tried a few variations:<br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_10.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_10"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_10.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_10"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_10" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> <br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_11.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_11"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_11.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_11"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_11" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <p>But eventually ended up with this cute little guy. With a whole pixel of headroom to spare!<br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_12.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_12"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_12.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_12"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_12" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> </li> </ol> <h2>Animation</h2> <p>Okay, so I'm getting the hang of things but now I need them to move. I went back to my original &quot;measuring stick&quot; guy and tried a walk cycle.</p> <img style="" border=0 src="/media/54967/stick_walk_straight.gif" alt="" /> <p>Not too bad. The torso stays straight up and down so I also tried angling it forward <em>and</em> backward to see how that changed the feel of the walk.</p> <img style="" border=0 src="/media/54967/stick_walk_forward.gif" alt="" /> <img style="" border=0 src="/media/54967/stick_walk_back.gif" alt="" /> <p>The forward tilted one feels like it moves faster, even though it doesn't, and feels like it has more of a sense of purpose about where it's going. The backward tilted one feels slower and like it's ambling, lazily, almost clumsily from place to place. Honestly, I feel like I could do almost anything with the legs and as long as I've got that bit of a head bob and the arm swinging, it'll pretty much read like a walk.</p> <p>Tried the little cute guy next and even added an idle animation and played with the position of his head.</p> <img style="" border=0 src="/media/54967/walk.gif" alt="" /> <p>Feet don't do much. Still looks like a walk. Nice.</p> <p>And finally, having just watched a video on sub-pixel animation I thought I'd give that a try in a small way by making a couple guys with weapons.</p> <img style="" border=0 src="/media/54967/bang.gif" alt="" /> <img style="" border=0 src="/media/54967/rocket.gif" alt="" /> <h2>1-bit</h2> <p>I decided to try making 1-bit black-and-white characters because I'd have to figure out how to suggest shape and separation of body parts with as few pixels as possible since I couldn't use colour to distinguish the sections. Mostly just rehashed everything I did above but in black-and-white only but a few things I learned: The colour stick-guy walking looked okay in all three versions. In 1-bit, the forward tilt of the body was <em>essential</em> to suggesting the direction of motion. Everything else just looked vaguely off no matter how I tweaked the frames.</p> <img style="" border=0 src="/media/54967/1bit_stick_walk.gif" alt="" /> <p>For characters with faces, outlining the head but leaving it open on one side worked best and, depending on the size of the head, eyes can go right up into the hairline or not. Also, I found for some reason that a black pixel <em>behind</em> the hand really helped to sell the idea of the hand hanging by the waist even though that black pixel didn't really represent a part of the character's body. More like just a shadow which helped to sell the idea of a hand there.<br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_13.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_13"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_13.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_13"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_13" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> <img style="" border=0 src="/media/54967/1bit_walk.gif" alt="" /></p> <p>And then, once I've got a general character I'm happy with I can always colourize the sprite if I want to.<br /> <table><tr><td width=64> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_14.png" width=64 height=64> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_14"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_14.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_14"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [16x16]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_14" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <h2>Textures</h2> <p>How do you make something look hard or soft? Rough or smooth? I have literally no idea. I started by just trying to sprinkle some random pixels around and try to figure out if it looked rough or smooth. What it looked like was a bunch of random pixels.</p> <p>So instead, I created a few regular patterns, copied them and then made some tweaks trying to change the texture somehow. What I mostly ended up with were just a bunch of different patterns. I guess some looked rough and some looked smooth but mostly they just looked either more or less regular.<br /> <table><tr><td width=128> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_15.png" width=128 height=128> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_15"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_15.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_15"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [32x32]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_15" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <p>As individual tiles they don't look like too much but some of them made nice patterns when tiled together. And I played around and found a few others that I liked as well:<br /> <table><tr><td width=96> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_18.png" width=96 height=96> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_18"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_18.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_18"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [24x24]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_18" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> <br /> <table><tr><td width=96> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_19.png" width=96 height=96> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_19"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_19.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_19"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [24x24]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_19" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> <br /> <table><tr><td width=96> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_20.png" width=96 height=96> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_20"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_20.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_20"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [24x24]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_20" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> <br /> <table><tr><td width=96> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_21.png" width=96 height=96> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_21"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_21.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_21"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [24x24]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_21" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> <br /> <table><tr><td width=96> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_22.png" width=96 height=96> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_22"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_22.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_22"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [24x24]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_22" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <p>Didn't really get me closer to texture though. So here's the process that I figured out, basically applying a manual noise filter, that's giving me fairly decent results. I think, anyway.</p> <p>I take copy of the &quot;clean&quot; tile and overlay some other pattern with a different colour. Usually diagonal lines but sometimes other patterns too, it takes some trial and error to find a good one. Anyway, once I've done that, I apply some rule like, &quot;all grey pixels in the top half of the tile get coloured white while all the grey pixels in the bottom half get coloured black.&quot; That gives me a &quot;dirty&quot; tile which still has some recognizable structure to it but is a bit messed up.<br /> <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_23.png" width=32 height=64> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_23"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_23.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_23"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x16]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_23" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <p>Tiled together on their own the dirty tiles are a bit too messy but mixing a few clean tiles in with them gives the eye enough to pick out the underlying pattern and make sense of the whole thing.<br /> <table><tr><td width=96> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_24.png" width=96 height=96> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_24"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_24.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_24"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [24x24]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_24" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <p>And then, just like with the characters, I can colourize them later. Here are a couple I quite like. Though I make no claims that my colour choices are any good or that these are great or anything. But, I think, not a terrible starting point, at least.<br /> <table><tr><td width=96> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_25.png" width=96 height=96> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_25"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_25.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_25"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [24x24]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_25" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> <br /> <table><tr><td width=96> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_26.png" width=96 height=96> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_26"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_26.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_26"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [24x24]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_26" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> <br /> <table><tr><td width=96> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_27.png" width=96 height=96> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_27"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_27.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_27"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [24x24]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_27" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <p>So that's the beginning of my pixel art journey so far. Still much to learn but I'm starting to get a handle on it.</p> <img style="" border=0 src="/media/54967/art-study.png" alt="" /> https://www.lexaloffle.com/bbs/?tid=43668 https://www.lexaloffle.com/bbs/?tid=43668 Sun, 04 Jul 2021 23:44:18 UTC Getting those jumps just right <p>I hate fiddling with meaningless magic numbers trying to get a behaviour right. I prefer a bunch of knobs with predictable effects. So I made this little tool for fine tuning variable-height jumping behaviour for platformers.</p> <p>Note: As written, it's possible for the cart to get stuck in an infinite loop for certain values so read below on how to, hopefully, avoid that issue.</p> <p> <table><tr><td> <a href="/bbs/?pid=94022#p"> <img src="/bbs/thumbs/pico8_jump_tuner_1-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=94022#p"> jump_tuner_1</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=94022#p"> [Click to Play]</a> </td></tr></table> </p> <h2>Usage:</h2> <ul> <li>Up/Down to select a value</li> <li>Left/Right to modify the value</li> <li>X to test the jump, tap for small jumps, hold for higher jumps</li> </ul> <p>There are three tunable values and a fourth value which is calculated for you and can't be changed manually. Once you get something you're happy with, note down the values, and use or modify the jumping code from this cart in your own project. I think I've clearly indicated all the parts of the code that can be deleted and which parts should be modified. If you do use it, attribution is appreciated but not necessary.</p> <p>The tunables are pretty self-explanatory. They are:</p> <ul> <li>height</li> <li>gravity</li> <li>initial acceleration</li> </ul> <p>The fourth value, alpha, is calculated based on the other three.</p> <h3>Height:</h3> <p>This should be a negative number (upwards direction) in pixels. It's actually more like &quot;requested height.&quot; Depending on the other values there is a certain amount of error in the calculations so the maximum height of the jump won't be exactly the height you specify but it should be close.</p> <h3>Gravity:</h3> <p>This should be a positive number (downwards direction). Technically, the units are pixels per frame per frame but that doesn't really matter. As you'd expect, this controls how fast you fall. Higher values make you fall faster; smaller values, slower.</p> <h3>Initial acceleration:</h3> <p>This should be a negative number (upwards direction). This is the upwards acceleration of the character when you press the jump button. While you're holding the jump button, this acceleration slowly decreases until the character is once again only under the influence of gravity. If you let go of the jump button before the jump reaches its full height, the acceleration is cut immediately and the character starts to fall sooner.</p> <h3>Alpha:</h3> <p>Alpha is calculated from the other three values and controls how quickly the initial acceleration decays.</p> <h2>Issues:</h2> <p>Given the specifics of the physics, it turns out that alpha can't be determined analytically: There's no nice simple formula for it. Or maybe there is and I'm just not patient enough to grind through the equations to figure it out...Anyway, it <em>can</em> be solved computationally, essentially using a binary search algorithm. Which is fine except that PICO-8's limited floating point precision as well as the way lua handles division by zero (turns out 0/0 = 32768, take that math!) means that the algorithm sometimes gets stuck in an infinite loop because it can't find a good enough solution.</p> <h3>Avoiding problems:</h3> <h4>Height error</h4> <p>Gravity and initial acceleration should have similar magnitudes, just in opposite directions. The greater the difference between them, the more error creeps into the requested height. Basically, if the difference is too great then once the upward acceleration cuts out, it takes longer for gravity to slow the character down and it over-shoots the target height. The closer the two are in magnitude, the closer to the target height you'll get. However...</p> <h4>Infinite loops</h4> <p>Though it is by no means obvious, when gravity and initial acceleration have the same magnitude the code which calculates alpha will try to divide by zero and the program won't be able to find a suitable solution. But it'll keep trying. Forever.</p> <p>PICO-8's limited floating point precision means that this actually become a problem when gravity and initial acceleration are even <em>close</em> to being the same magnitude. If gravity = 0.3 and acc=-0.3, that's going to cause problems.</p> <p>In general, gravity and acceleration should be close to the same size (but in opposite directions) if you want accuracy in height but not <em>too</em> close or the cart will freeze up.</p> <p>This is only a problem when <em>calculating</em> alpha based on the other values. When you copy/paste the code you'll just <em>assign</em> alpha and can tweak the other values to be as close as you want.</p> https://www.lexaloffle.com/bbs/?tid=43527 https://www.lexaloffle.com/bbs/?tid=43527 Fri, 25 Jun 2021 19:05:01 UTC Context-free generative string grammars <p>I've been playing around with various procedural generation techniques and wrote a little tool for creating generative string grammars. It's 104 tokens and the code is on <a href="https://github.com/jasondelaat/pico8-tools/tree/release/string-gram">github</a> under an MIT license.</p> <h2>Demos</h2> <p>This first one generates side-scroller/platformer levels. Each character of the generated string represent a four tile wide column of the map. I didn't add a player because I just wanted to showcase the level generation itself so the map just scrolls automatically from the start to the end. Reload the cart to generate a new level. It's just an example so the levels aren't particularly interesting but you could add a few &quot;post-production&quot; passes over the generated strings to clean things up, generate enemies, add additional rules for pre-made features, etc.<br /> <table><tr><td> <a href="/bbs/?pid=93825#p"> <img src="/bbs/thumbs/pico8_jdelaat_stringgram_scroller-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=93825#p"> jdelaat_stringgram_scroller</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=93825#p"> [Click to Play]</a> </td></tr></table> </p> <p>The next one is a vastly simplified version of <a href="https://mewo2.com/notes/naming-language/">this process</a> to generate names of things/places in a made-up language. The grammar constructs some syllables then makes words of either two or three syllables. A final pass over the generated word cleans up some messy, hard-to-pronounce double consonants. For a quick, not very sophisticated implementation it comes up with almost but not quite decent sounding place names.<br /> <table><tr><td> <a href="/bbs/?pid=93825#p"> <img src="/bbs/thumbs/pico8_jdelaat_stringgram_words-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=93825#p"> jdelaat_stringgram_words</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=93825#p"> [Click to Play]</a> </td></tr></table> </p> <h2>API</h2> <p>string-gram provides a number of functions to help you build a grammar and it's easily extensible via custom rule functions. Grammar rules are just functions which take no arguments and return a string.</p> <ol> <li> <p>lit</p> <p>The lit function takes a string as input and produces a rule which will generate that string when called:</p> </li> </ol> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>h = lit('hello') -- h is a rule, aka a function of 0 arguments. print(h()) -- prints 'hello'</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <ol start="2"> <li> <p>seq</p> <p>The seq function takes any number of rules as input and produces a new rule which outputs the result of each rule in sequence.</p> </li> </ol> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>h = lit('hello') c = lit(', ') w = lit('world') hw = seq(h, c, w) print(hw()) -- prints 'hello, world'</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <ol start="3"> <li> <p>choice</p> <p>The choice function takes any number of rules as input and outputs a new rule which outputs the result of one of those rules chosen at random.</p> </li> </ol> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>a = lit('a') b = lit('b') ab = choice(a, b) print(ab()) -- prints either 'a' or 'b' at random</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <ol start="4"> <li> <p>copy</p> <p>The copy function takes a single rule and an integer, <em>n</em> as input and outputs a new rule which applies the given rule <em>n</em> times.</p> </li> </ol> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>a = lit('a') aaa = copy(a, 3) print(aaa()) -- prints 'aaa'</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <ol start="5"> <li> <p>sym and register</p> <p>Grammars are often defined recursively so you may find yourself needing to include one rule, which you haven't yet defined, inside the definition of some other rule. sym and register solve this problem by allowing you to insert a 'symbolic' rule which will be looked up at a later time when it's actually called.</p> </li> </ol> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>one = lit('1') -- This won't work! -- many_ones = choice(one, seq(one, many_ones)) -- Instead, we use a symbolic rule... many_ones = choice(one, seq(one, sym('1s'))) -- ...and then use register to insert the rule into a lookup table. register('1s', many_ones) print(many_ones()) -- prints random number of 1s -- ex: 1, 111, 1111111111111, etc</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <ol start="6"> <li> <p>Custom rules</p> <p>Rules are just functions which take no arguments and return a string so you can easily create your own rules or functions which create rules. For instance, suppose you want to create a rule that randomly returns the output from another rule or else returns an empty string. One way to do that would be like so:</p> </li> </ol> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function zero_or_one(rule) return function() if rnd() &gt; 0.5 then return rule() else return '' end end end a = lit('a') a_or_not = zero_or_one(a) print(a_or_not()) -- prints either one 'a' or nothing with 50/50 probability</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> https://www.lexaloffle.com/bbs/?tid=43445 https://www.lexaloffle.com/bbs/?tid=43445 Mon, 21 Jun 2021 17:54:07 UTC prot-oo: Prototype based inheritance/OOP <p>This is the second of the two little utilities I've made, the first being stream-ecs (in a separate post.)<br /> <a href="https://github.com/jasondelaat/pico8-tools">The github project page.</a></p> <h2>prot-oo</h2> <p>This one's a bit more straight-forward. Prototype based inheritance/OOP. It basically just defines the base object and a few methods.</p> <p>Use create to use an object as a prototype for some other object:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dog = object:create() -- use the base object as prototype dog.sound = 'woof!' function dog:talk() print(self.sound) end cat = dog:create() -- use the dog object as prototype cat.sound = 'meow' dog:talk() -- woof! cat:talk() -- meow</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>You can create classes (well, not really, because prototypes, but sort of) by defining a method called init and using the new method.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>vec2D = object:create() function vec2D:init(x, y) self.x = x self.y = y end function vec2D:length() return sqrt(self.x^2 + self.y^2) end v = vec2D:new(3, 4) v:length() -- 5</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The new method creates a new object with vec2D as the prototype and then passes its arguments to init which initializes the object.</p> <p>And sub-classes:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>vec3D = vec2D:create() function vec3D:init(x, y, z) self.proto.init(self, x, y) -- calls vec2D's init self.z = z end function vec3D:length() -- overrides vec2D's length return sqrt(self.x^2 + self.y^2 + self.z^2) end v3 = vec3D:new(1, 2, 3) v3:length() -- 3.74-ish</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And that's about it. Enjoy!</p> https://www.lexaloffle.com/bbs/?tid=43339 https://www.lexaloffle.com/bbs/?tid=43339 Tue, 15 Jun 2021 20:39:41 UTC stream-ecs: ECS with auto queue management. <p>I picked up a copy of PICO-8 a few months ago on a whim and have been playing around with it, following a tutorial here and there, etc. Really enjoying it and I love seeing the creativity of people in the community.</p> <p>I have yet to try my hand at an actually complete game and, as I'm basically infinitely distractable, may never actually accomplish it. In the meantime, to learn some of the ins and outs of Lua, with which I was not previously familiar, I've combined two of my favourite software writing pastimes: re-inventing wheels and making tiny utilities.</p> <p>In that spirit, I give you:</p> <ul> <li>stream-ecs (267 tokens), a (sort of) reactive stream based entity-component-system with automatic entity queue management, and</li> <li>prot-oo (72 tokens), prototype based object-oriented programming (in a separate post)</li> </ul> <p>I'll describe them a bit below and there's more info at the <a href="https://github.com/jasondelaat/pico8-tools">github project page.</a> Each tool has its own README describing it in detail.</p> <h2>stream-ecs</h2> <p>I know there are a few different ECS frameworks floating around the PICO-verse. This one's larger than some and smaller than others but <em>should</em>, I hope, save you tokens in the long run by managing queues of game entities for you.</p> <p>You start by creating a &quot;world&quot; and then spawning systems off of it:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>world = ecs() world :system({'timer'}, increment_timer) world :system({'timer', 'position'}, move_character)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Each system creates and manages its own queue and when you add entities to the world...</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>world:insert(character)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>... the world passes it to each system which either stores it in its queue, if it matches the selectors, or discards it.</p> <p>You can also chain systems together to create a sort of filter. This example is functionally identical to the one above:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>world = ecs() world :system({'timer'}, increment_timer) :system({'position'}, move_character)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Entities with a timer component go into the first system's queue then, of those entities with a timer, those which <em>also</em> have a position component go into the second system's queue.</p> <p>You define components like so:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>position = component('position', {'x', 'y'})</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Which returns a constructor function:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>p = position(1, 2) p.x -- 1 p.y -- 2</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And you create entities like so:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>character = entity() :add(position(1, 2)) :add(timer(0))</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>There's a pre-defined draw component and draw system so you don't have to manage those manually either, you can just do this:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function _draw() cls() world:draw() end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And updating all your entities is this easy:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function _update() world:run() end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Here are a couple demos:</p> <h3>Simple particle system</h3> <p>Spawns a new particle at the source each tick. Particles are removed from the system after 200 ticks so there are 200 particles on screen at any given time.<br /> <table><tr><td> <a href="/bbs/?pid=93530#p"> <img src="/bbs/thumbs/pico8_jdelaat_streamecs_particles-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=93530#p"> jdelaat_streamecs_particles</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=93530#p"> [Click to Play]</a> </td></tr></table> </p> <h3>Simple platformer</h3> <p>Collisions are a bit wonky: the character occasionally falls into the floor some reason but that is, presumably, because of how I wrote collisions and not the ECS itself. It should be enough to give you an idea of how it works, anyway.<br /> <table><tr><td> <a href="/bbs/?pid=93530#p"> <img src="/bbs/thumbs/pico8_jdelaat_streamecs_platform-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=93530#p"> jdelaat_streamecs_platform</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=93530#p"> [Click to Play]</a> </td></tr></table> </p> https://www.lexaloffle.com/bbs/?tid=43348 https://www.lexaloffle.com/bbs/?tid=43348 Tue, 15 Jun 2021 20:39:25 UTC