Journey to Demi Daggers


Demi Daggers was born out of multiple ‘dead ends’:

  • too ambitious Quake engine to really fit Pico8 constraints
  • aborted 3d platforming game using a voxel world
  • objective to make non-violent games
  • scoresub in 2023

Note: as usual, knowlege is not lost!

Devil Daggers (short, DD) is a fast paced survival shooter, often seen as an ultra lean Quake. The game is both technically impressive (in its own retro way) and finely designed gameplay wise.

Pico8 is a fantasy console for game development - machine has some hard constraints (cpu, resolution...), yet a very fun platform to work with!

Head Tilt

Part of the Quake visual signature, the effect gives a very good sense of movement I decided to keep, regardless of its significant cpu price.
The in-game effect uses a basic screen shearing to mimic rotation:

  • copy screen to himem
  • poke blocks of 8 pixels at desired y location
  • hide top/bottom 8 pixels to hide the gaps 😜


Initial version used sspr and screen rebase, clocking at 14% - later rewritten using poke4 for half the price!

BSP+Bitplanes=❤️

OG game playground is not square - beyond the visual appeal, it forces the player to watch steps and attempt some daring moves!
How to render it efficiently, without sorting artifacts (inc. when player falls off ground)?
I haven’t spent the last few years in Doom/Quake codebase to miss the answer: a BSP! 

Each frame, camera position is pushed down the correct leaf and recursion does the sorting magic:

    -- mini bsp:
    --              0
    --             / \
    --           -1   world
    --           / \
    --       brush  -2
    --             /   \ 
    --          brush  brush


 World smallest BSP recursion:

_bsp[id]=function(cam)
  -- left/right from closure - BSP tree is generated at runtime
  local l,r=_bsp[left],_bsp[right]                
  if(cam.origin[plane_id]<=plane) l,r=r,l
  l(cam)
  r(cam)
end

Second must have effect are shadows.
They ensure player gets a sense of the danger zone when zipping around monsters, and help "ground" monsters in the world.

Shadows started as slanted ellipses based on player view position, rendered using bitplane masking to darken ground.
So far so good except when the approximation doesn’t really work or when the enemy is not visible but shadow should :/


Game was also missing the blood stains unti quite late. This is again a key design element from DD, highlighting your progress in the game.

In one of these ‘I’ll rewrite half of the game on a hunch’ moment, I decided to face one of the dark place of pico8: bitplanes! (and secret spritesheets).

After a bit of head scratching and multiple mini prototypes (bitplanes are tricky) I got the multi-pass rendering right:

First render pass:

  • reset palette
  • set bitplane to 0b11110110
  • draw stains on texture using colors 0b0101 or 0b0111 (never cleared)
  • set bitplane mask to 0b00001000
  • clear shadow layer via rectfill(0,0,127,127,0)
  • set bitplane mask to 0b10001000
  • draw shadows as regular circfill() with color 0b100

Second render pass (‘mode 7’) fixes colors using a "fade to black" palette based on distance (see below) and voilà!


top left: hardware palette/default palette - top right: floor software palette
Note how texture details are kept on both stains (light grey) & shadows (red/green)

Editor

For some reason (scoresub being one), I wanted the game to be BBS (or at least) Splore compatible.

I liked the appeal of shipping with the editor I used to craft game models and allow players to make their own multicart game.

The UI went through multiple iterations before settlings on a look&feel closer to the in-game menuing.

nope...nope...yes!

Leaderboard

Scoresub is a mythical Pico8 API, the one that will project Pico8 into the real "BBS" world.

Alas, 2023 was still not the year of online scores - Thanks Newgrounds to have such nice&free leaderboard support!

As usual, most of the work interfacing with the leaderboard API is in the HTML plate (see plate/fps.html in source code).

Upgrade Effect

That effect swaps between 16x2 palettes for a result I am quite happy with!
Each palette has a ‘moving’ red band that will be sampled based in distance:

With the fade to black, hit, fade to white palettes, game uses a total of 34 palettes!


All palettes are automatically generated using Python, both as a way to facilitate experiments and streamline integration into the carts.

Credits to Krystman on the shmup serie to point out plain white flash is dull, eg make sure hit highlight keeps sprite details.

Custom Controls

Long standing complaint on Poom was along the line of:

‘esdf? game sux’ (sic)

I have something to say about games only supporting wasd but 🙄

Back in the Poom days, Zep added that cryptic stat(28), that unfortunately was left unused in final build.


Given the fast pace of Demi Daggers, I really wanted to fix that and came up with what may very well be the first keybinding page for a pico8 game!

Collisions

Game uses 2d spatial partionning to speed up intersection/collision queries,m. Cell size is 32x32, larger than any enemy yet small enough to not have the whole cast on one cell!

Grid coordinates are packed in a 4 byte value:

z|x>>16 -- y is ‘up’ for various reasons 😜

 Enemies can registers into all cells they touch, used for cheap boids behavior and precise bullet/enemy collisions. Makes up for a pretty gif when mapping cell occupancy to color (brighter: more enemies):


Daggers are much faster than monsters and have a zero radius. I resorted to raycasting to ensure perfect collisions while scanning the least amount of cells:

-- scans grid from a to b using normalized vector (u,v)
function collect_grid(a,b,u,v,cb)
  local mapx,mapy=a[1]\32,a[3]\32
  -- check first cell (always)
  -- pack lookup index into a single 16:16 value
  local dest_idx,map_idx=b[3]\32|b[1]\32>>16,mapy|mapx>>16
  cb(_grid[map_idx])
  -- early exit
  if dest_idx==map_idx then    
    return
  end
  local ddx,ddy,distx,disty,mapdx,mapdy=abs(1/u),abs(1/v)
  if u<0 then
    -- -1>>16
    mapdx,distx=0xffff.ffff,(a[1]/32-mapx)*ddx
  else
    -- 1>>16
    mapdx,distx=0x0.0001,(mapx+1-a[1]/32)*ddx
  end
  
  if v<0 then
    mapdy,disty=-1,(a[3]/32-mapy)*ddy
  else
    mapdy,disty=1,(mapy+1-a[3]/32)*ddy
  end
  while dest_idx!=map_idx do
    if distx<disty then
      distx+=ddx
      map_idx+=mapdx
    else
      disty+=ddy
      map_idx+=mapdy
    end
    cb(_grid[map_idx])
  end
end

The Hand

Mistakes were made...

Hey, game is not "first person perspective" for nothing, let's use real life action!

Needless to say what version made it to the final build :)

The Sound

DD has a very (very?) particuliar sound design - Demake could not have happened without @ridgekun mad PICO8 sfx&music skills!

Read it all on: https://nerdyteachers.com/PICO-8/Pico-View/?issue=11

Final Thoughts

Game release went ok, with good reception, but missed most of the social media relay that Poom had (less fame to surf on I guess, Twitter in flame...).

We also went through significant launch tweaks, thanks to players like Werxyz, making the game more approachable but likely too late.

At the time of writing, the game had disapeared from the charts :/ But hey, that's indiedev life!

Another take away is to avoid making games I cannot finish (both original and demake) - it makes some design decisions more random than coming from first hand experience.

---

Thanks Ridgek for the fun!
(and still a big no for “Fighty Fingers”!)

Thanks Heraclum, Miss Mouse & the Pico8 Discord band for being such a nice community!

Source code & tools at: https://github.com/freds72/daggers

Get Demi Daggers

Download NowName your own price

Comments

Log in with itch.io to leave a comment.

Wow. Such a great work! Congrats ^^

Really great thread!! Thanks for sharing!!

(+1)

thanks

You’re welcome!!

Awesome to read. Thank you for sharing!

thanks - hope some info could be useful for your next games!

(+1)

This was a pretty interesting read thank you for this.