Adam Richardson's Site

CCCCCC the sequel to BBBBBB

Table of Contents

cccccc_1.gif

Figure 1: CCCCCC game play gif

Introduction

I wanted to revisit my VVVVVV inspired game BBBBBB. I decided to make a sequel using PICO-8 and named it CCCCCC, naturally. Being a sequel the gameplay needed to be expanded. I added two different enemy types, a horizontal and a vertical moving enemy and 16 different levels. This is probably one of the most complete games I have made and I am very happy with the results.

How to Play

You emerge from the purple door and your goal is to reach the blue door. Your character is a green stick man that can move left or right with the arrow keys. You are not able to jump but you can change the direction of gravity. This will allow you to navigate the obstacles and reach the blue door. If you fall off the edge or if you bump into an enemy you will reset at the purple door. In the upper left hand corner you can see which level you are on in white text. In the upper right hand corner you can see the number of times you have died. You can play the game here.

cccccc_000.png

Figure 2: Zoomed out view of the PICO-8 map editor

Utilizing the Map Editor

The map in PICO-8 is 128x32 cells. Each cell is then an 8x8 sprite. To keep things simple I restricted all sprites to be 8x8. One of my goals when making this game was to utilize the map editor in PICO-8 as much as possible. Besides setting the location of the game world entities from the map editor I also wanted to be able to set the background effect. To do this I first decided that each level will be comprised of a 16x16 grid of cells. I would then place the sprites using the PICO-8 map editor on in that particular level. At the start of each level I would calculate when section of the map needs to be parsed by level index global. Sprites for that indicate the background effect and the enemy sprites would then be erased (covered up with a black rectangle) from the map. Only one sprite for the background effect is respected for the level. The cells where enemies are found on the map are considered their starting points. The game would then update the enemies per frame and ensure that they are drawn at the correct location.

cccccc_001.png

Figure 3: Close up view of the PICO-8 map editor

Conclusion

Initially I was attempting to do collision detection between the player rectangle and the map cell. After about a week of trying to fix various bugs; I decided to parse the platform blocks into a table at the start of each level. This was a good idea because it allowed me to use the map editor to define the background effect and enemy locations. The only annoying part was that I could not find a quick way to move the map over by 16 cells. This made it easy to accidentally place cells on the wrong level. I also couldn’t find a way to just erase a sprite from the map, I always had to pick another empty sprite to replace it. All in all, the map editor greatly sped up the level creation and I think it was the right approach.

pico-8 cartridge // http://www.pico-8.com
version 27
__lua__
-- CCCCCC
-- bY aDAM RICHARDSON

player={
   x=0,
   y=0,
   w=8,
   h=8,
   sidx=0,
   sdir=1,
   sw=1,
   sh=1,
   anim_t=0,
   flipping=false,
   should_flip=false,
   resting=false,
   anim_time=0.08}

grnd_s=3
exit_s=16
start_s=17
fire_s=18
star_s=19
v_enem_s=6
h_enem_s=4
level=0
start_grav=2
grav_speed=2
grav_mag=1
bg_c=0
blocks={}
exit={}
start={}
speed=1
bg_ticks=1
bg_t=0
bg_s=0
death=0

enemies={}
-->8
-- util
function collide(r1,r2)
   return r1.x<(r2.x+r2.w) and
      (r1.x+r1.w)>r2.x and
      r1.y<(r2.y+r2.h) and
      (r1.y+r1.h)>r2.y
end

function move_player(x,y)
   clear_rect({
         x=player.x,
         y=player.y,
         w=player.sw*8,
         h=player.sh*8,
   })
   player.x=x
   player.y=y
   spr(
      player.sidx,
      player.x,
      player.y,
      player.sw,
      player.sh,
      false,
      grav_mag<0)
end

function anim()
   if(player.anim_t+player.anim_time<t()) then
      player.anim_t=t()
      player.sidx+=player.sw*player.sdir
      if player.sidx>player.sw*2 then
         player.sidx=player.sw
         player.sdir*=-1
      end
      if player.sidx<0 then
         player.sidx=player.sw
         player.sdir*=-1
      end
   end
end

function clear_rect(r)
   rectfill(
      r.x,
      r.y,
      r.x+r.w,
      r.y+r.h,
      bg_c)
   local lvl_x=(level%8)
   local lvl_y=flr(level/8)
   map(lvl_x*16,lvl_y*16,0,0)
   for e in all(enemies) do
      rectfill(
         e.o.x,
         e.o.y,
         e.o.x+e.o.w,
         e.o.y+e.o.h,
         bg_c)
   end
end

function nomap_clear_rect(r)
   rectfill(
      r.x,
      r.y,
      r.x+r.w,
      r.y+r.h,
      bg_c)
end

-->8
-- starfield
star_cnt=25
function star_bg()
   if stars==nil then
      stars={}
      for i=1,star_cnt do
         local s=flr(rnd(2))
         add(stars,{
                x=128+flr(rnd(128-s)),
                y=flr(rnd(128-s))+s,
                r=s})
      end
   end
   cls(bg_c)
   for s in all(stars) do
      local spd=((s.r+2)/2)
      s.x-=spd
      if s.x<0 then
         s.x=128+flr(rnd(128-s.r))
         s.y=flr(rnd(128-s.r))
      end

      rectfill(
         s.x,
         s.y,
         s.x+s.r,
         s.y+s.r,7)
   end
end
-->8
-- fire
function init_fire()
   fw=64
   fh=12
   psize=3
   pixels={}
   for x=0,fw do
      for y=0,fh do
         pixels[idx_xy(x,y)]=0
      end
   end

   for x=0,fw-1 do
      pixels[idx_xy(x,fh-1)]=psize
   end

   colors={
      0,
      8,
      10}
end

function idx_xy(x,y)
   return y*fw+x+1
end

function update_fire()
   for i=0,(fw)*(fh-1) do
      local src=i+fw
      local p=pixels[src]
      if(p==0) then
         pixels[src-fw]=0
      else
         local r=band(flr(rnd(3)),3)
         local dst=src-r+1
         pixels[dst-fw]=p-band(r,1)
      end
   end
end

function fire_bg()
   if pixels==nil then
      init_fire()
   end
   update_fire()
   local offset=128-fh
   for x=0,fw-1 do
      for y=0,fh-1 do
         for z=0,1 do
            pset(x+(z*63),y+offset,colors[pixels[idx_xy(x,y)]])
         end
      end
   end
end
-->8
-- game functions
function reset_player()
   move_player(start.x,start.y)
   grav_mag=1
   player.resting=false
   player.flipping=false
   player.should_flip=false
   death+=1
   sfx(2)
end

function fall()
   if player.resting then
      return
   end

   local r={
      x=player.x,
      y=player.y,
      w=player.w,
      h=player.h
   }
   r.y+=(grav_speed*grav_mag)
   player.flipping=true
   if r.x<0 then r.x=0 end
   if r.x>120 then r.x=120 end

   for b in all(blocks) do
      if collide(r,b) then
         player.resting=true
         player.flipping=false
         player.should_flip=false
         if grav_mag>0 then
            r.y=b.y-8
         else
            r.y=b.y+8
         end
         break
      end
   end

   move_player(r.x,r.y)

   if player.y<0 or
      player.y>120 then
      reset_player()
   end
end

function move_lr(s)
   local r={
      x=player.x+s,
      y=player.y,
      w=player.w,
      h=player.h}
   player.resting=false
   for b in all(blocks) do
      if collide(r,b) then
         if s<0 then
            r.x=b.x+b.w
         else
            r.x=b.x-r.w
         end
         break
      end
   end

   move_player(r.x,r.y)
end

function level_start()
   player.anim_t=0
   cls(bg_c)
   if level==16 then
      return
   end
   local lvl_x=(level%8)
   local lvl_y=flr(level/8)
   map(lvl_x*16,lvl_y*16,0,0)
   blocks={}
   enemies={}
   for i=0,15 do
      for j=0,15 do
         local x=lvl_x*16+i
         local y=lvl_y*16+j
         local s=mget(x,y)
         if s==grnd_s then
            add(blocks,{
                   x=i*8,
                   y=j*8,
                   w=8,
                   h=8
            })
         elseif s==exit_s then
            exit={
               x=i*8,
               y=j*8,
               w=8,
               h=8}
         elseif s==start_s then
            start={
               x=i*8,
               y=j*8,
               w=8,
               h=8}
         elseif s==star_s or
            s==fire_s then
            bg_s=s
         elseif s==v_enem_s or
            s==h_enem_s then
            add(enemies,{
                   x=i*8,
                   y=j*8,
                   w=8,
                   h=8,
                   s=s,
                   mag=1,
                   o={
                      x=i*8,
                      y=j*8,
                      w=8,
                      h=8}})
         end
      end
   end
   player.x=start.x
   player.y=start.y
   grav_mag=1
   sfx(3)
end

function update_enemies()
   for e in all(enemies) do
      clear_rect(e)
      if collide(player,e) then
         reset_player()
      end
      if e.s==h_enem_s then
         e.x+=e.mag
      end

      if e.s==v_enem_s then
         e.y+=e.mag
         if e.y>128 then e.y=0 end
      end

      for b in all(blocks) do
         if collide(b,e) then
            e.mag*=-1
            sfx(1)
            break
         end
      end

      if e.x<0 or e.x>(128-e.w) or
         e.y<0 or e.y>(128-e.h) then
         e.mag*=-1
         sfx(1)
         break
      end
   end
end

-->8
-- p8 functions
function _init()
   level_start()
end

function _update60()
   if level==16 then return end
   if btn(⬅️) or btn(➡️) then
      if btn(➡️) then
         move_lr(speed)
      else
         move_lr(speed*-1)
      end
      anim()
   else
      player.sidx=1
   end

   if btnp(❎) and not player.flipping then
      grav_mag*=-1
      player.resting=false
      player.should_flip=true
      player.flipping=true
      sfx(0)
   end

   fall()
   update_enemies()
   if collide(player,exit) then
      level+=1
      level_start()
   end
end

function _draw()
   if level==16 then
      cls(1)
      print("you win!",50,50,7)
      return
   end
   bg_t+=1
   if bg_t==bg_ticks then
      bg_t=0
      if bg_s==star_s then
         star_bg()
      elseif bg_s==fire_s then
         fire_bg()
      end
      move_player(player.x,player.y)
      pset(127,0,0)
      for e in all(enemies) do
         local s=e.s
         if e.mag<0 then s+=1 end
         spr(s,e.x,e.y)
      end
   end
   print(level,0,0,7)
   print(death,120,0,8)
end