Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Creating the Scrolling Background Effect from Battletoads
Jess
Jess

Posted on • Edited on

Creating the Scrolling Background Effect from Battletoads

Who doesn't love/hate the Turbo Tunnel level in Battletoads? I decided to analyze and recreate the scrolling background as seen in the gif below:

Battletoads (1991) Turbo Tunnel level

My crude re-creation in Pico-8. Check out that uncanny likeness. 😉

Here is a screenshot from the actual game. If you look closely you can see the edges of the sprites and where they are repeating. I indicate some spots with a rectangle and white arrows.

Alt Text

Here is the background I created showing how my sprites are repeating.

Note: Examples are written using Lua in the Pico-8 Fantasy Console. Functions include excerpts of code relevant to my explanation and aren't necessarily the complete code.

Bear with me as I explain my initial thought process for scrolling the background, before I deleted all that code and did something else.

First I decided to create separate arrays for each background section. The array would hold at least enough elements to cover the width of the screen. Each element would be an object that kept track of the width of the sprite, the x1 value (top left of the sprite), and the x2 value (top right of the sprite).

I did something like the code below on my first pass.first_row,second_row, etc are tables, which is the data structure in Lua used to create objects and arrays. Ininit_bg() I looped from 0 to 7 to create the tables for each sprite row.add(first_row, { x1 = i * 16, w = 16, x2 = i * 16 + 16 }) pushes the table of values to thefirst_row table. Since 7 * 16 = 112 that puts the last sprite at an x value of 112, and since the sprite is 16px that covers the 128px width of the game screen. The tables containing 32px wide sprites ends up creating more sprites than needed to cover the screen but it didn't really matter if there were a few extra offscreen.create_land() does a similar thing, but it loops 10x and creates 2 tables: one for the land top and another for the bottom.

function_init()-- speeds to control how fast each row is movingtop_bg_speed=5bottom_bg_speed=6middle_bg_speed=1land_speed=5-- tables for each row of spritesfirst_row={}second_row={}land_top={}land_bottom={}third_row={}bottom_row={}init_bg()endfunctioninit_bg()fori=0,7doadd(first_row,create_spr_table(i,16))add(second_row,create_spr_table(i,32))add(third_row,create_spr_table(i,32))add(bottom_row,create_spr_table(i,16))endcreate_land()endfunctioncreate_spr_table(i,w)return{x1=i*w,w=w,x2=i*w+w}endfunctioncreate_land()fori=0,10doadd(land_top,create_spr_table(i,32))add(land_bottom,create_spr_table(i,32))endend
Enter fullscreen modeExit fullscreen mode

In the_update() function that runs 30fps, I looped through each table to set the newx1 andx2 values. Each row has a speed variable that can be adjusted depending on how fast you want the sprites to scroll. In Battletoads, when you first get on the Turbo Bike the sprites in the foreground and background move at slightly different speeds and gradually ramp up to look like they're going at the same speed because you're going so fast it would be indistinguishable. In my .gif above you'll notice I kept mine at different speeds because I liked the effect, but I could easily make each section go at the same speed by changing the variable values in_init().

As each sprite moved to the left I needed to know when the sprite was offscreen so it could be removed from the table. I also needed to know thex2 value of the last sprite in each table so when I pushed a new sprite to the table I could use thatx2 value for the new sprite'sx1 value. The tables for the land sprites had to be handled differently than the rest because it doesn't endlessly repeat and needed to be started offscreen. Some rows are made up of sprites that belong together and are updated together using a loop for one of the tables.

function_update()-- only some rows are shown here, the rest are basically the same-- update first row valuesfori=1,#first_rowdofirst_row[i].x1-=top_bg_speedset_new_x2(first_row,i)end-- update second/third row valuesfori=1,#second_rowdosecond_row[i].x1-=middle_bg_speedthird_row[i].x1-=middle_bg_speedset_new_x2(second_row,i)set_new_x2(third_row,i)end-- update land valuesfori=1,#land_topdoland_top[i].x1-=land_speedland_bottom[i].x1-=land_speedset_new_x2(land_top,i)set_new_x2(land_bottom,i)end-- remove element once it goes out of viewif(is_offscreen_left(first_row[1].x2))del_first_value(first_row)ifis_offscreen_left(second_row[1].x2)thendel_first_value(second_row)del_first_value(third_row)end-- add element to end of table if there's an empty space between the last element x2 value and end of the game screenif(should_add_bg_spr(first_row[#first_row].x2))add_bg_spr_to_end(first_row,16)ifshould_add_bg_spr(second_row[#second_row].x2)thenadd_bg_spr_to_end(second_row,32)add_bg_spr_to_end(third_row,32)end-- update landif(is_offscreen_left(land_top[#land_top].x2))reset_land()endfunctionreset_land()fori=1,#land_topdolocalstart=i*32+128land_top[i].x1=startland_top[i].x2=land_top[i].x1+32land_bottom[i].x1=startland_bottom[i].x2=land_top[i].x1+32endendfunctionadd_bg_spr_to_end(tbl,w)localx1=tbl[#tbl-1].x2add(tbl,{x1=x1,w=w,x2=x1+w})endfunctionshould_add_bg_spr(x)-- 148 for a smoother addition since it's out of viewreturnx<=148endfunctiondel_first_value(tbl)del(tbl,tbl[1])endfunctionis_offscreen_left(x)returnx<=0endfunctionset_new_x2(tbl,idx)tbl[idx].x2=tbl[idx].x1+tbl[idx].wend
Enter fullscreen modeExit fullscreen mode

In the_draw() function that also runs at 30fps, I looped through each table again to draw each sprite at thex1 values of each table element. There is a row in the middle that's static so there's no table for it. I just loop 8 times (0 - 7) to fill the screen with 8 static sprites each frame. Since the land has end pieces it is handled differently here as well. As I looped through the land tables, if the index was 1 (Lua indexes begin with 1, not 0) or the index equivalent to the length of the array, I set the sprites to the end pieces. If it was the last index I also setflp to true, which controls whether the sprite should be flipped on the x axis.

Note: In Pico-8 a sprite is drawn using spr(sprite_number, x, y, w, h, flip_x, flip_y) where w is the number of sprites wide and h is the number of sprites tall to draw. Each sprite is 8x8 so in order to draw a 16x16 image, you would set w=2 and h=2

function_draw()cls()-- draw top rowfori=1,#first_rowdospr(64,first_row[i].x1,0,2,2)-- first_rowend-- draw second/third rowsfori=1,#second_rowdospr(66,second_row[i].x1,16,4,4)-- second rowspr(70,third_row[i].x1,48,4,2)-- third rowend-- draw static middle rowfori=0,7dospr(96,i*16,64,2,2)end-- draw landdraw_land()-- draw bottom rowfori=1,#bottom_rowdospr(204,bottom_row[i].x1,112,2,2)endendfunctiondraw_land()fori=1,#land_topdolocalflp=falselocaltop_spr=196localbottom_spr=200ifi==1ori==#land_topthentop_spr=192bottom_spr=232endif(i==#land_top)flp=truespr(top_spr,land_top[i].x1,64,4,4,flp)spr(bottom_spr,land_bottom[i].x1,96,4,2,flp)endend
Enter fullscreen modeExit fullscreen mode

This all created the end result perfectly. Then I figured why add/remove from the table when I can make a set amount (enough to cover the 128px width + at least 32px extra offscreen) and just change thex values as each sprite goes offscreen. Before I bothered to try that, it dawned on me that I actually don't need to do any of this. 🤦‍♀️ I don't need tables at all. I can just create everything in a single loop with a couple of variables that keep track of the x values for each row.

Let's Try This Again

I deleted all the tables and the functionality that went with them and created a few new variables:top_startX,middle_startX,bottom_startX, andland_startX. These values will control where each row begins. They are initialized to 0 and as the speeds are subtracted from each row every frame they will become negative until they reach -128; then they will be reset to 0. Well, except forland_startX which I'll explain in a bit.

The only thing_update() needs to do now is subtract the speeds from the starting x values for each row.

In_draw() there is a loop where each row is drawn, minus the land. Where the sprites are at least 32px wide I was able to just draw the sprite because it would create the sprites beyond the 128px screen width. For example, if i = 7,spr(66, i * 32 + middle_startX, 16, 4, 4) creates the sprite at an x value of 224 plus whatevermiddle_startX is equal to. As I explained above, it will be subtracted each frame, which is what makes the sprites look like they're scrolling to the left. Where the sprites were only 16x16 there wasn't any coverage for the right side as the sprites moved to the left before the next loop. I would have to loop to at least 16 (instead of 7) for full coverage, so instead I drew additional sprites at the x value + 128 (the screen width).

For the land, I had to create aland_endX variable that was equal to the length of the land + 1 multiplied by 32 (the width of the land section). Then I resetland_startX once it was<= -land_endX. Instead of resetting it to 128 I added agap_start parameter so I can change the distance between each piece of land if I want to.

This is the full code below to create the scrolling effect. It's much simpler and cleaner than the previous method using tables and it works the same.

function_init()top_bg_speed=5bottom_bg_speed=6middle_bg_speed=1land_speed=5top_startX=0middle_startX=0bottom_startX=0land_startX=0endfunction_draw()cls()palt(0,false)-- make black visiblepalt(1,true)-- make darkblue transparent-- draw bg (minus land)fori=0,7do-- top row 16x16spr(64,i*16+top_startX,0,2,2)spr(64,i*16+top_startX+128,0,2,2)-- second row 32x32spr(66,i*32+middle_startX,16,4,4)-- third row 32x16spr(70,i*32+middle_startX,48,4,2)-- static middle rowspr(96,i*16,64,2,2)-- bottom row 16x16spr(204,i*16+bottom_startX,112,2,2)spr(204,i*16+bottom_startX+128,112,2,2)end-- draw landdraw_land(20,128)-- resetif(x_should_reset(top_startX))top_startX=0if(x_should_reset(middle_startX))middle_startX=0if(x_should_reset(bottom_startX))bottom_startX=0endfunctionx_should_reset(x)returnx<=-128endfunction_update()top_startX-=top_bg_speedmiddle_startX-=middle_bg_speedbottom_startX-=bottom_bg_speedland_startX-=land_speedendfunctiondraw_land(length,gap_start)localland_endX=(length+1)*32-- +1 for off screen padding-- reset-- gap_start is what px you want the next piece of land to startif(land_startX<=-land_endX)land_startX=gap_startfori=1,lengthdolocalflp=falselocaltop_spr=196localbottom_spr=200if(i==1ori==length)then-- end piecetop_spr=192bottom_spr=232endif(i==length)flp=true-- right end piecespr(top_spr,i*32+land_startX,64,4,4,flp)-- topspr(bottom_spr,i*32+land_startX,96,4,2,flp)-- bottomendend
Enter fullscreen modeExit fullscreen mode

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software Engineer / Flatiron School grad
  • Location
    NJ
  • Education
    Flatiron School
  • Pronouns
    she / her
  • Work
    Software engineer
  • Joined

More fromJess

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp