CCCCCC the sequel to BBBBBB
Table of Contents
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.
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.
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