Log In  


Making PICO Space

Cart #drakeblue_picospace-0 | 2021-04-03 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
116

This is a rambling description of some of what went into making PICO Space. I've tried to write it for most readers to follow - there's some basic stuff and nothing very advanced. Hopefully it's not too dull and might help someone.

When You Wish Upon a Starfield

Coming up to Christmas of 2020 I had been spending most of my dev time trying to squeeze image data into PICO-8 to make a Dungeon Master clone (I promise I will return to this at some point). I'd got a bit tired of writing compression and encoding routines and feeling like I was fighting PICO-8 rather than playing nicely with it. I'd seen some other nice star-fields and particles in other peoples' games and wondered how much it would take to do my own.

I'd also read about the CAMERA function and suspected that would help - using CAMERA to transform the "view window" once instead of transforming the positions of every single particle many times seemed like it'd be a big win in terms of performance.
I knocked up the following while the girlfriend was watching a Christmas movie to try it out. Ironically, the stars in the final game don't benefit from the CAMERA function at all - although the other particles and everything else do.

Cart #drakeblue_camerastars-0 | 2021-04-07 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
10

There's not a lot to it: use directions to "fly" the red line around (there's nothing there, but the star field). But this is the basis of how the ship flies in PICO Space.

I wrote a post about the stars and fade effect here: https://www.lexaloffle.com/bbs/?tid=41149

Cart #fading_stars-5 | 2021-04-12 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
14

I considered a rotate and thrust method, like in games like Thrust, Oids etc. but decided to keep it simpler, friendlier to those who didn't grow up in the 80s and allow the game to be more dynamic. Left, right and thrust would have saved a precious PICO-8 button though...

In the final version, there's a constant deceleration added to the velocity components of the ship (which gets wiped out if you hold down the controls) - and a very low minimum velocity. Why?

Not Just a Triangle; Not Even a Triangle

The ship (and the NPC ships) are all drawn in the game using a combination of lines and a sprite to cover the hole in the middle (yes, really - I had some triangle drawing code sat in the p8 file for weeks without ever actually using it in the end so it got cut). The vertices used for the lines are calculated from a normalised vector of the ship's velocity. If the velocity gets too close to zero then this calculation, the vertex calculations and the lines don't come up with anything sensible and the ship doesn't draw correctly.

-----------------------------------------------------------------------------------------
-- draws the player or an npc ship
function draw_ship(s)
 ship_pal(s)
	-- calc some vertices based on current heading angle
	--    0
	--   (0)
	--  2   1
	local z,an=s.z<1 and -1 or 1,s.an
	local s0,s1,s2,c0,c1,c2=sin(an)*z,sin(an-0.15)*z,sin(an+0.15)*z,cos(an)*z,cos(an-0.15)*z,cos(an+0.15)*z
	local svx0,svy0,svx1,svy1,svx2,svy2=3.5*c0,3.5*s0,-3.5*c1,-3.5*s1,-3.5*c2,-3.5*s2

	line(svx0*1.2,svy0*1.2,svx1*1.2,svy1*1.2,2)
	line(svx0*1.2,svy0*1.2,svx2*1.2,svy2*1.2)
	line(svx1,svy1,svx2,svy2)

	line(svx0,svy0,svx1,svy1,8)
	line(svx0,svy0,svx2,svy2)
	line(0,0,svx1,svy1)
	line(0,0,svx2,svy2)
	line(0,0,svx0,svy0)

	spr(unpack_split'0,-4,-4') -- cockpit
	reset_dr_pal()
end

ship_pal sets up the correct colours for whatever ship is being drawn - there's a pair of values for every brighter colour apart from the blue used for the cockpit.

The z variable deals with the "flip" ability that the ship has so you can face backwards - otherwise, while the ship does have some inertia it will always be drawn to point in the direction it's moving.

I'll explain more about "unpack_split" in another post.

Here's the ship without the cockpit sprite:

reset_dr_pal is a function that acts like a call to pal with no arguments, but doesn't affect the screen palette. I've used it for a few projects now:

function reset_dr_pal()
 poke4(0x5f00,0x0302.0110,0x0706.0504,0x0b0a.0908,0x0f0e.0d0c)
end

The latest PICO-8 letting consecutive values be multiple pokes at a time has let me optimise this so it's a bit opaque even to me. IIRC it dumps the equivalent of 0,1,2,...,15 into the correct part of PICO-8's memory to reset the draw palette and whatever magic needs to be done for palt too(?), but doesn't hit the screen palette range of memory.

Feel free to lift this for your own code if you think it'll be useful - no warranty expressed or implied ;)

I tweaked the ship drawing code a few times, but it never varied much. Once I had a ship flying about I needed somewhere for it fly to so I made a "planets" table that was populated with random coordinates, drew a circular planet sprite and hit go. That's gone now - it was handy to have as a placeholder, but I don't miss it.

Start of a Solar System

Once I had planets it made sense to me to add a sun/star, but I wanted it to be bigger than the planets. I'd already used 64x64 of the sprite sheet with the planet sprite so I wondered what I could do with PICO-8's circfill command.

It turns out that circ and circfill let you draw pretty large circles for not a great deal of perf cost. I tried various sizes and settled on an 800 pixel radius circle for the sun with some extra circles around it. One is a border, the others are there just because I like how they look. My "game design" excuse is that they give some warning to the player that they're about to fly into a star - not v healthy for their ship.

I have another cart that draws a partial circle within another circle to show the sun on the scanner, but the token count was too much. In the end, the sun on the scanner is drawn by generating lots of points then checking if they are within both the circle of the sun and the circle of the scanner; then drawing a circle of radius 1.

Originally I had yet another circle for the edge of each system - but as the systems got busier the performance cost was just too high so I needed something a bit more "sophisticated". I noticed that the edge circle was so large that it never looked like anything else but a straight line on the little PICO-8 screen so in the game I do a little bit of maths to work out two intersection points with the edge circle and draw a line between them i.e. the edge circle segment is approximated with a line.

--edge of system
if g_dist_to_sun>g_edge-23 then
 -- approximate with drawing tangent line at angle through ship's coordinates
 -- since ship is so far from centre and edge circle is so large
 -- edge is a circle around 0
 -- get angle to ship
 local an,ed=atan2(S.x,S.y),g_edge+rnd'16'
 -- get tangent angle at 90deg and coords of point on edge (with some jitter)
 local an2,ca,sa=an-0.25,cos(an)*ed,sin(an)*ed 
 -- get vector of line using tangent angle
 local ca2,sa2=cos(an2)<<7,sin(an2)<<7
 line(ca-ca2,sa-sa2,ca+ca2,sa+sa2,p%8+8)
end

I use g_dist_to_sun to conditionally draw the sun when needed as well.

Having a circular edge seemed like a nicer way to stop the ship flying too far out of the game than just clamping to a rectangle or wrapping the coordinates. I had a really fancy bit of code that would turn the ship around from anywhere outside the system and force it back. In the end just adding a vector pointing towards the centre of the system to the ship's velocity is what makes it behave like a stuck blue bottle behind glass in the game.

  g_dist_to_sun=dist_trig(S,g_sys.sun)
  if g_dist_to_sun>g_edge then
   local avx,avy=get_dir(S,g_sys.sun)
   vx+=avx
   vy+=avy
  end

Trigonometry for Distances

(As any fule kno) the distance between a point A and B is the square root of the sum of the squares of the coordinate distances i.e. d = sqrt(dxdx+dydy) -- for 2D

Except not in my game.

In a fit of madness I decided to make the systems pretty large, 10240 pixels in radius, in fact. PICO-8 numbers are a 16-bit fixed point type with a range of -32768.0 to 32767.9999847412109375 (0x8000. 0000 to 0x7fff). Which means I was fine until I needed to calculate a large-ish distance and had to square numbers over 1000 e.g.

dist({0,0},{9000,8000}) = sqrt(9000*9000 + 8000*8000) = sqrt(81000000 + 64000000)

Um...

Weirdly, I got a really long way into coding before this actually became a problem and I'm not quite sure how that happened - especially since I was expecting it all along, I'm no stranger to the limitations of number types or even to fixed point floats.

One method I tried to get round this was to divide every value (bit shift in fact) before the squaring operation and then shift back afterwards. I'd lose precision, but in theory it'd work fine i.e. something like:

dist = sqrt( (dx>>4)*(dx>>4)+(dy>>4)*(dy>>4) )<<4

Most of the time I'd only needed the shifted squared value. But I had real trouble getting it to work well - possibly because of other code around it. And sometimes I wanted the true distance (with the sqrt) anyway.

I'd been doing some experimentation with the SIN and COS functions and I'd noticed that they're very fast on PICO-8 and I wondered about whether this might be another solution.

A bit of trig later and I had a candidate:

-- distance with trig
-- to avoid large numbers
-- not v accurate, but seems good enough.
-- also seems v fast on pico-8
function dist_trig(a,b)
 local x,y=abs(b.x-a.x),abs(b.y-a.y)
 if x<y then x,y=y,x end -- accuracy goes down massively if x is much smaller than y so swap them :)
 return x/sin(atan2(y,x))
end

I discovered the accuracy problem mentioned in the code a bit later - it manifested as the ship spontaneously diving into the sun if you flew too close to the top or bottom of it - and only those two points. The code to stop the ship escaping the edge of the system was firing and pushing the ship into the centre of the system because the distance calculation was so bad it thought the ship was at the edge not right by the sun!

Anyway, this function gets used all over the place now and was favourably comparable in accuracy and speed to any of the shifted distance functions that I tried.

Galaxy Generation

Of course, the more sensible solution would probably have been to make the star systems smaller and "zoom" the coordinates only for drawing, but I'd already done most of the work on galaxy creation and drawing and didn't really want to change it too much. I think I actually tried it for 30 minutes, made a mess and then gave up.

When you choose a new game or load from a saved game in PICO Space it generates the whole galaxy from an integer seed (your captain's name is a different value; otherwise they couldn't travel to other galaxies...).

The stars are all generated from the galaxy seed plus a fraction part e.g. the fourth star has something like id=galaxy_id+4*0.0001. This means there are potentially 65535 galaxies (I think, or at least 32767) with every system having a unique id/seed "in the gaps".

Apart from star number 1 which is always at (0,0), the stars are given random positions within a fixed distance from that star with the condition that they can't be too close to an already existing star. This is v fast.

Then the code links the galaxies with wyrmholes (called gates in the code). First it makes a minimum spanning tree with wyrmholes:

  • find the star with minimum distance from star 1
  • add wyrmholes to link the two
  • add the new star to star 1 in a "found" set
  • find the closest star to any star in that set
  • link with wyrmholes
  • repeat last 3 steps until every star is in the "found" set

This way I know that every system is connected to the galaxy as a whole. Without this some systems aren't reachable (I had considered having a jump drive upgrade that let you jump between groups of stars - maybe next time).

Unfortunately my algorithm to calculate the minimal spanning tree gets v slow after relatively few iterations (checking the closest star in an n-size set of stars has a pretty bad O value) and acts as a mechanical limit to how big the galaxies can get.

I'd be more worried about it, but for two things:

  1. The loading view covers it well. It looks like the stars are being generated, but it's actually showing the stars being added to the minimum spanning set.
  2. It's fast enough that even with 70 stars it's not that slow and I think 70 stars is probably enough.

--controversial?--I've noticed in a lot of procedurally generated games there's a whole load of repetitious content where there doesn't really need to be - it doesn't add much to the game and feels a bit like it's there just because the developers could. Now PICO Space is still pretty guilty of this - there's a lot of space in PICO Space and 70 systems is probably still way more than most players will ever visit. The systems don't have that much variety - I really wanted to put in more and kinda of heart-breakingly PICO-8 is easily fast enough to allow it. But there's not much cart space and I just couldn't find the tokens. There were a lot of things I would have added without the token limit and even some stuff I had to cut before release. I never found myself wanting to add more star systems. In fact, for a lot of dev time I reduced the number to make testing faster.--/controversial--

Anyway, long story short: I didn't waste any more time (or tokens) optimising this, instead I slapped in a logo and a few star spr calls and pretended it was a deliberately designed intro sequence...

Once that's done, there are a few iterations after the spanning tree search looking for short distances between stars and adding more gates between them so that there are "loops" and it's a bit easier to get about. This is pretty fast too.

Generating Systems

Then the systems themselves are fleshed out. Originally I generated each system in the galaxy only when the player entered them. In fact, all I had was the current system and some wyrmholes in it. When the player entered a wyrmhole I took the seed value associated with it and generated the new system. I even added a wyrmhole back to the previous system using its seed to give the illusion of persistence. For the galaxy map I was going to run this generation on the fly when it was opened to a certain depth of wyrmholes and that way I'd have a near to infinite galaxy and tiny memory footprint!

Typing it out now it still seems like it might have worked, especially as I know the scope of what's in the game. At the time though, I ditched that idea since I didn't. The final straw was wanting to put "missions" into the game.

Each system has wyrmholes and planets which also may have space stations orbiting them. The wyrmholes, only really have a position, a destination id and a draw function. They are called wyrmholes because I was hoping to have "space wyrms" in the game. Sadly, that never happened.

Planets

Planets have radii and colours and are added in distance "slots" (with the wyrmholes) radiating out from the sun - this way there shouldn't be any collisions between them, even as the planets orbit around the star. Yep, the planets do orbit in the game, but their positions only update when entering a system. I had them update along with everything else for a long time though.

As mentioned elsewhere in this post, the scale of each system is quite large. The planets orbit in circles determined by sine and cosine functions combined with their distance from the star (I balked at setting up stable systems with realistic gravity calculations if only because I'd disappeared down enough wyrmholes already). Unfortunately, the precision of these functions isn't high enough to allow smooth movement when any distance from the sun. It caused the planets to "jump" between positions on their orbit. Unperturbed I wrote a routine to interpolate between the imprecise positions so that the planets would still orbit smoothly. It worked really well and I was sad to have to cut it to regain the tokens it needed.

I was able to leave the scribbly planetary rings in place though, which are drawn by generating a bunch of vertices using sine and cosine values (with random jitter) and drawing lines between pairs of them. Draw the back half of a ring first, draw the planet, then draw the front half of the ring.

Like the planet interpolation, I designed them in a separate test cart first:

Cart #drakeblue_rings-0 | 2021-04-08 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
10

The code in the game is very similar. Knocking up little test carts is nice in PICO-8 since it's v lightweight to get going. Doing this also means less messing up of existing code, especially if you change your mind and it means less flying about trying to find the thing you want to test.

Space Stations

The space stations do orbit as you fly, around the planets. A planet has a station entry in its table so there's only one per planet (early in dev I had up to 4!) and the space station positions get updated with everything else that updates. When you're docked the ship has to match this (that was a fun bug).

The space stations have three different sprites, a few circles and a pal call in their draw function to provide some cosmetic variety. I had written a little cart which would allow me to specify shapes, sprites and map commands in a string that was interpreted at runtime to give many more looks for them, but tokens and performance stopped me using that. And I hit the compressed size limit quite badly too.

At one time there was a ship's garage with mechanic NPCs in every station but alas tokens...

Missions

(kinda)

I wanted to add a whole different variety of these, but eventually I only made the passengers on the noticeboards at the stations - the simplest missions I could think of. They're even more basic than fetch quests (find something and bring it back - sounds complicated!) or even deliveries (plausibly you only have room for one passenger in your small ship so I only needed to track one mission at a time). I ran out of tokens before I could attempt adding any more types.

To make the passenger missions work I needed to have some knowledge of not just the name of the destination system, but also of the objects in the system; specifically the space stations. I'd hoped to have docks on planets, but, again, the token limit got the better of me.

It was so much easier to have the destination systems pre-created and in memory for this. PICO-8 has a generous 2MB for such data (my first computer had 64KB and David Braben and Ian Bell made do with a fraction of that). PICO-Space only ends up using about half of that at any time (when it's working correctly).

The game takes a seed of the current space station id and the progress that the player has made through the game and uses that to generate a number of notices with destinations and a seed for the animal wanting a ride. That way, if you return to the notice board it keeps the same notices, at least until you complete a mission.

When you finish enough journeys a progress global is increased, an upgrade is awarded, different news & NPC messages are unlocked and more dangerous weevils may appear. There's an upper limit to how many journeys are needed for each progress point (four I think) so every player should progress reasonably quickly.

Silly Names

Nonsense word generators are something I've written a few times in the past. They're very effective (and entertaining) for something so easy to do. If you've never written one I highly recommend it, especially for newer programmers.

There is a single list of syllables and a single function that generates all the names in PICO Space. If I had more space/tokens then having different syllable sets etc. per species or system or something might have been fun, but I never felt like this was a weak part of the game and garnered an embarrassingly large amount of amusement out of some of the silly things it came out with.

------------------------------------------------------------------------------------------
-- generate some wacky random names
function gen_name(s)
 stash_n_set_seed(s)
 local n,nm=rnd_spl_str"1|2|2|2|3|3|3",''
 for i=1,n do
  if i>1 and rnd(15)<1 then
   nm..=' '
  end
  nm..=rnd(g_syls)
 end
 unstash_seed()
 return nm
end

The gist of this is:

  • choose length for the name in number of syllables
  • for each syllable up to thate length choose a syllable string and append it
  • occasionally append a space instead of a syllable

The stash_n_set_seed function is my hack to get around the trials and tribble-ations of working with a random number generator for procedural generation (as opposed to a noise function). See below for more about this.

Here's the syllables:

split"ca,bal,da,gar,non\-en,pol,der,arp,bun,duc,kit,poo,v\-evee,zir,buf,v\-evil,xan,frak,ing,out,re,far,do,tel,tri,cry,quack,er,dog,pup,sno,ger,bil,pa,n\-ena,jan\-en,es,on",--g_syls

For more serious names, don't put in such silly syllables e.g. poo.

Ms and Ws

The eagle eyed amongst you may have noticed that PICO Space has "wide" 5x5 pixel M and W letters instead of the usual 5x3 characters that PICO-8 has by default. My other games, e.g. P8C-BUN have similar wide Ms and Ws too. In those games, I have a custom print function that iterates through whatever string is to be printed and when it finds an M or W it replaces it with a sprite that has the wide version of the letter. I added this after I got some negative feedback about the legibility of the text in a game.

Originally PICO Space used the same function, but I found it was a bit of a performance problem. PICO-8 has a much more "spiky" processing time per frame than my other games have had.

Fortunately, zep's addition of P8SCII in the newer version of PICO-8 came at exactly the right moment and allowed me to replace my routine with these magic codes:

m=>  n\-en [print an n then shift the cursor back 2 pixels (\-e) and print another n] 
w=>  v\-ev  [u\-eu looks okay too]

As long as a find/replace is done for those characters in a string to be printed then it "just works" and I was able to ditch my custom print routine at last. This can be done "offline" even. The only tricky part was trying to centre text when the width was no longer as predictable - I do have a mostly working solution for that. Ask me if you're interested.

Taming RND

PICO-8 has no noise function, but it does have RND and a seed set function SRAND. I considered writing a noise function, but I suspected I'd want the tokens for something else and I was concerned about performance. I'd already used RND for the first hacky progress in the game when I thought about this so I gave up on noise and set up a function that saved the current random number (or at least whatever RND returned at that moment) and called SRAND with a value I'd passed to it. Then I was careful with the order I called RND in and did a lot of testing to make sure that I got consistent or random results as appropriate.

For a particular seed, RND always returns the same sequence of subsequent results so, for instance, setting the seed before choosing portrait sprites and generating a bunch of names using RND means you'll always get the same output. That's why there's always the same animal minding each station and the same notices each time you look on a station's noticeboard (at least until you ferry another passenger). I'd restore a "random" seed after using RND in this way so that, for example, the NPC generation stayed unpredictable.

-- set seed and store a random number for later
function stash_n_set_seed(seed)
 g_old_seed=rnd()
 srand(seed)
end

-- restore "randomness"
function unstash_seed()
 srand(g_old_seed)
end

This works pretty well all considered and I never felt I was having performance problems from it. I call the time function for the galaxy seed and pilot id just to mix it up even more.

Animals

I needed characters for the game and I considered identikit faces like Elite 2 uses for a bit. In the end, I drew some animals as placeholders and then never looked back.

Animals feature in my games. Get over it.

I wanted the portraits to move so initially I was going to add animation frames, but horizontally flipping was easier and meant I could have more unique pictures. I figured it was "good enough".
[8x8]

There are 59 animals in total.

News and NPCs

The Beeb News was my first attempt to communicate some story in the game and add a bit of narrative colour (alongside the colour colour). The news stories develop as the player progresses in the game - there's a different set after each upgrade is achieved. I used a very basic template filling function to generate a bit of customisation for each game - e.g. the galaxy name and the president's name change depending on which galaxy you're in.

------------------------------------------------------------------------------------------
-- substitutes values (#n) into a string for the corresponding entry in data (data[n])
function template_fill(t,data)
 local i,out=1,''
 while i<=#t do
  local c=sub(t,i,i)
  if c=='#' then
   i+=1
   out..=data[tonum(sub(t,i,i))]
  elseif c=='m' then
   out..='n\-en'
  elseif c=='w' then
   out..='v\-ev'
  else
   out..=c
  end
  i+=1
 end
 return out
end

You'll notice the M and W substitutions in this - in the last week of development I had to add this to reduce the size of the strings used for the news and NPC chat in order to fit the game under PICO-8's compressed size limit. Fortunately, it works very well and turned out to be quick enough.

The news is drawn using the same scrolling message system that is used for the upgrades and notice board, just with different colour parameters.

----------------------------------------------------------------------
-- draws a bar of scrolling text
function draw_scroller(scroller,y,x,c,bc)
 clip(x,y,128,y+6)
 rectfill(x,y,128,y+6,bc)
 print(scroller.message,x-scroller.x,y+1,c)
 clip()
 scroller.x+=1
 if scroller.x-x>scroller.len then
  scroller.x=-64
  return 1
 end
end

NPCs

The other attempt at story telling comes from the NPC ships. These will send you random messages when they're on-screen. Like the news, the template function adds a bit of customisation e.g. for the current system and there are different sets of messages for each stage of progress in the game.

The NPCs are (obviously) drawn in the same way as the player ship, but on top of basic attributes like position, velocity, colour etc. they also have a target that they head towards; either a station or a wyrmhole. They're spawned from a station or from a random position (with hyperspace particles) to pretend that they've just jumped in. I'd love to have more persistent NPCs, but I probably spent too many tokens, performance and cart space on them already.

One of the more interesting problems to solve was how to stop NPCs spawning in a position then flying towards a target on the other side of the system's sun without them hitting it. I don't check them for collisions so they'd just fly right on through blissfully unaware of any problem. Given the player can't do that it was a bit non-ideal to witness ships emerge from the superhot plasma if you happened to be flying close to the sun at the time. It got even more obvious when I stopped pausing the simulation in the System Map since you could watch the NPCs sun-diving:

I tried various clever solutions with trigonometry and mathematics, but they never worked very well. In the end, I solved this problem with a hack. Whenever an NPC is generated their journey is quickly simulated, start to finish, but with coarser framerate. If they get too close to the sun then that journey is ditched and the NPC journey is generated again until a valid journey is produced. This does make for the occasional performance spike, but on the whole works quite well and for few tokens.

There's a cap on the number of NPCs per system that takes into account the number of weevils to try and make sure there's quite a bit of performance headroom, just in case.

Enemies

To be honest, I was having so much fun making a pretend universe that I left it quite late to add in a challenge to the game. Eventually I admitted that I needed villains so I drew on personal history.

A few years ago we found weevils in our kitchen. All over our kitchen. Eventually we tracked them back to a bag of birdseed that now moved and crackled by itself...

The next hours and days consisted of rooting out weevil and destroying it - weevils became a kind of bogeyman in our house. Nothing is more evil than weevil in our home.

[0x0]

I wanted to make each weevil unique, but in the end they are all based on the same code with varying attributes. As the game goes on more and more dangerous weevils are generated ahead of the ship within a random circle depending on how dangerous the system is and the difficulty level of the game. Some weevils can hit the ship and damage it with their collision, all weevils fire missiles at the ship at different rates and for different amounts of damage.

Space is Big, even PICO Space

One of the biggest problems with the weevils was how to make them possible to fight on the tiny 128x128 PICO-8 screen without making them too slow to stop a player just flying past them. I didn't want to slow down the player either, since getting between locations was taking long enough already. Besides, when I tried slowing the ship, it just felt... bad.

I think the weevils are still a bit too fast to be fair - I'm not convinced that I really solved this problem entirely.

WARNING SPOILERS - DO NOT READ IF YOU WANT TO LEAVE THE ENDING AS A SURPRISE

Weevil Queen

I wanted an ending and a "boss" and so I did some research into big weevils (as you do). It turns out that there are some big ones out there and there are even some that live like ants with a queen. Not to the same scale as ants though.
Add a bit of poetic licence and I came up with the weevil queen that's in the game.
What do you mean: "there isn't one?"
Are you saying you've not completed enough missions to gain the Prototype Scanner upgrade, located the Weevil Base and jumped into the system to fight the Weevil Queen and save the galaxy?
At time of writing, I'm not sure anyone has except me (many testing times). If you have I'd really like to know (please?).

The Weevil Queen is generated in a system that has only one wyrmhole in it (a leaf node in the galaxy "tree") and is by default hidden in the Galaxy Map (and the wyrmhole to it is hidden in the system containing it too). Some of the changes for the weevil system happen during galaxy generation. Most of it is done on entry. There are no space stations or NPC ships and the weevil queen is added. She behaves a lot like a normal weevil, but she has hitboxes at the back of her that don't take damage and she moves more slowly. She fires the most deadly missiles, most frequently. She may also teleport randomly if hit.
The weevil queen is drawn using several sprites together and she gyrates at a speed according to how close to destruction she is.
[0x0]
[0x0]

function(q)
     pal(15,0)
     local en=5-q.life/10

     local sn48,cs48,sn32,cs32=sin(p/48*en),cos(p/48*en),sin(p/32*en),cos(p/32*en)

     q.x+=sn32

     circ_orig(p%96,g_sys.sun.pl[q.life\10+1])

     spr(135,-25+sn48,-10+cs48,2,2) -- right front leg
     spr(135,11-sn32,-10-sn48,unpack_split'2,2,1') -- left front leg
     spr(135,-29,cs32-52,2,2,nil,1) -- right back leg
     spr(135,13,cs48-52,unpack_split'2,2,1,1') -- left back leg
     spr(137,12+sn48,-26+sn32,unpack_split'2,2,1,1') -- left back leg
     spr(137,-27-cs32,-26+cs48,2,2,nil,1) -- right back leg

     spr(128,sn48-15,unpack_split'-50,2,4') -- right wing
     spr(130,-sn48,unpack_split'-50,2,4') -- left wing
     spr(132,cs48-13,unpack_split'-29,3,4') -- thorax
     spr(139,-10+cs48,-8-sn32,3,3) -- head

     reset_dr_pal()
   end

When she is destroyed, she is changed into the Intergalactic Wyrmhole that leads to the next (harder) galaxy, the weevil system loses its weevil flag and all the weevils are destroyed.

I spent a lot of time getting all this to work, all the time wondering if anyone will ever see it.

Tables and "Objects"

I'm not much of an object oriented zealot (learning to program in BASIC in the 80s does that to you), but for the entities in PICO Space I used tables as objects of a kind.
Things like planets, stations, weevils (called aliens in the code) and NPC ships generally have "standard" attributes (x,y for position, bounding box for draw culling etc.) and functions (update and draw).
On entering a system an "updateables" and a "drawables" table is populated with appropriate things from those tables in the current star system and the update and draw functions loop through these and call them as appropriate. Using the CAMERA function I set the origin for drawing to reflect the object's position then the draw function draws from there. Things with a map function get drawn on the System Map etc. It works quite well and feels a bit like adding components to entities.
I had a lot more separate lists and loops for these early on just to get going - I find doing things "the dumb way" first and improving it later works well for me (along with, if it ain't broke don't fix it...).

Some things e.g. particles are kept separate still, for speed.

Particles

There are two big types of particle in PICO Space. Points and circles. There's a function to add both of these in a single line each. They have a lifetime, position and update functions.

Points have two update functions - a typical velocity update one and a more abstract randomly swapping x and y velocity update. The former is used in the explosions. The latter is most obvious in the hyperspace cloud at the very start of the game.
Circle particles draw at a different size corresponding to the life left of the particle.

Why do they look the way they do? It's the screen fade effect.

See https://www.lexaloffle.com/bbs/?tid=41149 for more information.

Menus

All the menus use the same code and are driven by data passed to them. There's nothing amazingly clever about them (or that), but I found it was much nicer in every respect to work with them once I took this approach. So I suggest writing generic menu routines soon if you find you're needing more than one of the things in a project - don't put it off.

Memory and Performance

PICO-8 makes both of these things really easy - Ctl/Cmd P along with a constant print of stat(0) if it went above a threshold value (usually 1024).
The latter helped me find a nasty problem very near release. For a long time I'd paused the game except when flying the ship in space, but particles were drawn and deleted in the draw function. When I enabled the game to update, even while in the maps and docked, I was generating particles (NPC ship engines, NPC hyperspace, sun flares), but never deleting them. So I'd run out of memory. Sometimes after a very long time. It happened much faster when I moved the cursor about in the map views so I spent ages debugging those to no joy only to eventually realise that I was running out of memory due to the ship engines firing a bunch of particles out into my dwindling memory...

Loading and Saving

I was very worried about getting the load and save functions to work, especially with the web player, but in the end it was very easy. Dump some values into cartdata to save, read them back when loading. I'm presuming the browser cache gets these when playing in a browser(?).
One slight downer is the paste codes don't work on the web. They seem to work fine in "native" PICO-8 though and the "binary" versions. It's the only thing that doesn't work there.

The best thing about having save games was for testing during development. I built up a text file with a long list of different saves that allowed me to test different stages in the game, different galaxies and systems etc. I could edit them a bit to quickly test some things as well.
I'd highly recommend implementing something like this if you have a longer game and want to test it - especially since it's something your players might be able to use too.

Tokens

As you may have gathered if you read this far, I ran out of tokens.
Procedural generation produces content from code in preference to pre-defined content in storage so it wasn't overly surprising. I've seen others struggle with memory for proc gen, but I found the 2MB limit wasn't ever a problem.

A large amount of the end of development involved trying to save tokens.

Look out for my followup post about that, working title: "A desperate programmer's attempts to squeeze galaxies into a PICO-8 cart"

--
If you have any questions about the above post or anything else to do with PICO Space please ask them below and I'll try to answer them (or maybe even extend this article even further).

10


1

When I saw that star prototype I wasn't expecting this to be the summary of such an advanced PICO-8 game, but I really enjoyed reading this. There was a lot of interesting ideas, details and workarounds written here, so thanks for sharing!



[Please log in to post a comment]