? How to use Random's Big Dumb Library: Game Engine 2 ● SmileBASIC Source

Sign In

Register
*Usernames are case-sensitive
Forgot my password

How to use Random's Big Dumb Library: Game Engine 2

This is part 2 of a 3 part tutorial on how to make a basic game with Random's Big Dumb Library. Other parts: Part 1, Part 3 (coming soon) This is a continuation of How to use Random's Big Dumb Library: Game Engine. You should read over it if you haven't already. We're going to add to the final program on that page. So we have a game; it does stuff. Let's make it better. We're going to change some parameters, give the player a follower, and use textboxes to display dialog when you find a chest.

More Game Parameters

The game engine has a lot of global parameters to change how things function. Remember how we changed the minimum collision tile with VAR("3:GAMECLSNSTART")=1? The same can be done for the following values: 'NAMEOFVARIABLE=DEFAULTVALUE 'What it does '-------------------------------------------------------- GAMETILESIZE=16 'Can change to 8 or 32 to match your tile sizes. GAMELAYERS=15 '4 bit field to set layers used by engine. Set layers will all update automatically as player walks. Defaults to "all" GAMECLSNLAYER=0 'The layer used to detect collision GAMECLSNSTART=1024 'The minimum solid tile ID. All tiles >= this will be blocked GAMECLSNMASK=&HFFFFFFFF 'The collision mask used by the map. You can set this to specific values so only certain things collide with the map GAMEDEBUG=FALSE 'Debug mode. Enable to get more information from the game GAMECPADDEADZONE#=0.15 'Minimum CPAD offset before values are used GAMECPADMAX=0.85 'Maximum CPAD offset (all higher will be truncated) GAMEPLWALKSPD#=1 'Player walking speed (pixels per frame) GAMEPLRUNSPD#=2 'Player running speed GAMEPLRUNBTN=#B 'Button used for running GAMEPLDIAGONAL=TRUE 'Allow/disallow diagonal movement (even cpad diagonals) GAMEPLPROCESS$="GAMEPLDEFAULT" 'Player callback function. Defaults to a simple log of actions GAMEPLCONTROL=TRUE 'Whether or not the player has control right now (useful for cutscenes) GAMEPLFOCUS=TRUE 'Whether or not game updates will focus on the player (useful for cutscenes) We've already used some of these before: you just do the VAR thing. For instance, if you wanted to make the player run EVEN FASTER you could do: VAR("GAMEPLRUNSPD#")=3 'OH JEEZ

A follower

What's an old lady without a cat? We're going to setup a cat to run a simple follow AI with collision detection and walking animations. The cat will follow the old lady if she goes too far and will simply walk in a straight line towards her.

Generic NPC Callbacks

First, we'll need to setup the cat processing function. Unlike the player, who has a complex function automatically applied with GAMEPREPAREPLSIMPLE, all other entities must supply the functionality themselves. Don't worry though: the walking and collision functions are already programmed so we just have to call those from our function. A game engine entity function takes no parameters. It is called once per frame. As with all other callbacks, it must be COMMON DEF. Our cat function should update its position so it follows the player, then call the walking function so it does all the crap it's supposed to. We're going to use the DISTANCE library function to calculate the speed the cat will travel, then use the CHASECALC function to automatically determine a new position for the cat so it "chases" the player. Finally, we'll send the data off to GAMESPWALKER so the cat will perform collision detection and walking animations automatically. Remember, we're just continuing off this code, so think of this as an addition. COMMON DEF CATPROCESS 'No parameters 'GAMEGETSPXY retrieves the world location of the given sprite. 'CALLIDX is the current sprite (it's a SMILEBASIC thing) 'SP is the player (remember we set it before) VAR CX#,CY#,PLX#,PLY# 'Store the cat and player locations GAMEGETSPXY CALLIDX OUT CX#,CY# GAMEGETSPXY SP OUT PLX#,PLY# 'Now we have the locations of the cat and the player VAR DIST#=DISTANCE(CX#,CY#,PLX#,PLY#) VAR SPD#=MIN(5,DIST#/20) 'The cat's speed will increase as the player gets farther away (up to a point) VAR CVX#,CVY# 'Store the cat's velocity IF DIST#>=16 THEN CHASECALC CX#,CY#,PLX#,PLY#,SPD# OUT CVX#,CVY# 'Figure out cat velocity based on simple chasing AI VAR STEPPED,FACING 'Some variables produced by the walking function (we might use them later) GAMETOSCREEN CALLIDX,1,CX#,CY# 'Update the cat's position on screen (we may have moved; this may not be necessary in the future) GAMESPWALKER CALLIDX,CVX#,CVY#,FALSE OUT CX#,CY#,STEPPED,FACING 'Perform all the walking stuff. END First, we get the positions of the player and the cat using the library function GAMEGETSPXY. Just pass it the sprite ID and it'll produce the world coordinates for that sprite. Sprite callbacks set the global CALLIDX to the current sprite ID. Since this is the process for the cat, we can count on it being the cat's sprite ID. This system is builtin to SmileBASIC. We stored the player's ID in SP in previous code and use it here. We then call the library function DISTANCE to figure out the distance between the cat and the player. The cat will walk faster as it gets farther from you: the SPD# variable is directly proportional to DIST#. The max is 5 though so it doesn't look like a cat rocket. Note: don't let NPCs go faster than 1 full tile (16 in our case), as the collision will stop working. Then if we're sufficiently far from the cat (DIST#>=16) we run the CHASECALC library function to compute the velocities. You give it the chaser coordinates, the "destination" coordinates (the player), and a speed and the function spits out the velocities for each direction for that frame. With all our information calculated, we make two final function calls: one to update the cat sprite's position relative to the world, and one to perform the animation and collision. GAMETOSCREEN updates the given sprite's screen coordinates based on the given layer and world coordinates. All entities use "world" coordinates so they are positioned relative to the BG, but to put them on the right place on screen, we have to convert the position to screen coordinates. The layer used for collision determines the world coordinates, so we pass 1 and the cat's coordinates so the sprite is in the right screen position BEFORE we calculate collision. This is VERY important: ALWAYS update the screen position BEFORE performing collision. GAMESPWALKER runs walking animations and collision detection on the given sprite moving at the given velocity. The final parameter is whether or not to draw collision hitboxes. This is only useful for debugging, so we set it to FALSE. Just like the player callback is given a bunch of data, GAMESPWALKER also produces data we can use. For instance, we could do something per cat step by checking the STEPPED variable afterwards. We're all done though, so we don't use the data.

NPC Setup

Setting up an NPC is nearly the same as setting up the player. The only difference is that the preparation function takes a callback, unlike the player. There are no defaults for NPCs, which is why we had to write it ourselves. VAR CSP=SPSET(856) 'Cat is sprite definition 856 SPSCALE CSP,2,2 'Make the cat the same size as the lady (woah) GAMEPREPARESP CSP,"CATPROCESS",16*X[0]+8,16*Y[0]+8,2,&HFFFFFFFF 'Put the cat in the same place as the old lady to start. GAMESETWALKDATA CSP,856,4,6 'Old lady's step distance was 8; cat is 6 because legs are shorter lol.

Follower Complete Code

We're not done yet though: we don't want the cat to be on top of the player. We can fix that with an SPOFS for the player and cat: the cat will then be forced underneath the player by using a lower Z value for the cat. I put the cat function at the bottom with the other callbacks. The cat setup is underneath the player setup. Each one gets a Z offset with SPOFS (both are negative so they show up in front of the BG layer). 'Initial setup. You have to EXEC my library (any slot other than 0 will do) ACLS EXEC "PRG3:RNDLIBFULL" XSCREEN 2,100,2 'Use 100 sprites and 2 BG layers (first 2 is screen mode) '--Map Preparation-- VAR I FOR I=0 TO 1 BGSCREEN I,100,100 'Set layer size to 100x100 BGSCALE I,2,2 NEXT BGFILL 0,0,0,99,99,99 'Fill first layer with grass lol (grass is ID 99; first two 99's are the ends) BGFILL 1,0,0,99,99,101 'Fill the collision layer with rocks (will become perimeter) BGFILL 1,1,1,98,98,100 'Fill the inner map with trees (exclude perimeter) '-Path and Chest Generation- VAR SNAKEP$[0] 'The snakepath parameter dictionary SNAKEP$=NEWSNAKEPATH$() 'Create the snakepath parameter dictionary VAR X[0],Y[0] 'The locations for each point (we'll use these later) FOR I=0 TO 15 PUSH X,10+RND(80) 'Random point at least 10 inside the walls PUSH Y,10+RND(80) IF I>0 THEN SNAKEPATH X[I-1],Y[I-1],X[I],Y[I],SNAKEP$,"CLEARPATH" NEXT FOR I=1 TO 15 STEP 2 'Start at 1 so we don't put a treasure right under the poor old lady BGPUT 1,X[I],Y[I],864 'See, the locations were useful NEXT GAMEPREPAREBG 3,1 'Use layers 0 and 1 (bitfield), and layer 1 is the collision layer VAR("3:GAMECLSNSTART")=1 'Any tile with ID 1 and above will trigger collision VAR("3:GAMEPLPROCESS$")="PLPROCESS" 'This is how you'd register the callback so the library calls your function '--Player Preparation-- VAR SP=SPSET(796) 'Define our sprite. Use dynamic sprite ID allocation for ease of use SPSCALE SP,2,2 'Scale sprite up to 2X size SPOFS SP,0,0,-20 'Put player closer to the top GAMEPREPAREPLSIMPLE SP,16*X[0]+8,16*Y[0]+8,1,&HFFFFFFFF GAMESETWALKDATA SP,796,4,8 VAR CSP=SPSET(856) 'Cat is sprite definition 856 SPSCALE CSP,2,2 'Make the cat the same size as the lady (woah) SPOFS SP,0,0,-10 'Put cat below player (but still above BG) GAMEPREPARESP CSP,"CATPROCESS",16*X[0]+8,16*Y[0]+8,2,&HFFFFFFFF 'Put the cat in the same place as the old lady to start. GAMESETWALKDATA CSP,856,4,6 'Old lady's step distance was 8; cat is 6 because legs are shorter lol. VAR CHESTS=8 WHILE TRUE VSYNC LOCATE 0,0 PRINT FORMAT$("%D chests left ",CHESTS) 'Tell player how many are left. CALL SPRITE 'Run all sprite attached functions. Our player is one 'The main loop is the place to put win condition checking. Sprite callbacks can't halt this loop. IF CHESTS<=0 THEN ACLS BGMPLAY 5 PRINT "Found all chests!" 'Hooray, you won! BREAK ENDIF WEND 'The path clearing function. It HAS to be COMMON so my library can use it COMMON DEF CLEARPATH X,Y BGFILL 1,X,Y,X+1,Y+1,0 END 'Player processing function COMMON DEF PLPROCESS SP,BGX,BGY,X#,Y#,VX#,VY#,FACING,STEPPED 'Player is standing on BGX/BGY. Together with FACING, we figure out which tile we're looking at. IF BUTTON(1) AND #A THEN 'They pressed the A button VAR IX,IY GAMEVECTOR FACING OUT IX,IY 'Convert FACING into unit offsets IX=BGX+IX IY=BGY+IY 'Now IX/IY hold the location the player is facing IF BGGET(1,IX,IY)==864 THEN 'Player clicked A on chest BEEP 3 'Might as well give feedback CHESTS=CHESTS-1 BGPUT 1,IX,IY,0 'Remove chest ENDIF ENDIF END COMMON DEF CATPROCESS 'No parameters 'GAMEGETSPXY retrieves the world location of the given sprite. 'CALLIDX is the current sprite (it's a SMILEBASIC thing) 'SP is the player (remember we set it before) VAR CX#,CY#,PLX#,PLY# 'Store the cat and player locations GAMEGETSPXY CALLIDX OUT CX#,CY# GAMEGETSPXY SP OUT PLX#,PLY# 'Now we have the locations of the cat and the player VAR DIST#=DISTANCE(CX#,CY#,PLX#,PLY#) VAR SPD#=MIN(5,DIST#/20) 'The cat's speed will increase as the player gets farther away (up to a point) VAR CVX#,CVY# 'Store the cat's velocity IF DIST#>=16 THEN CHASECALC CX#,CY#,PLX#,PLY#,SPD# OUT CVX#,CVY# 'Figure out cat velocity based on simple chasing AI VAR STEPPED,FACING 'Some variables produced by the walking function (we might use them later) GAMETOSCREEN CALLIDX,1,CX#,CY# 'Update the cat's position on screen (we may have moved; this may not be necessary in the future) GAMESPWALKER CALLIDX,CVX#,CVY#,FALSE OUT CX#,CY#,STEPPED,FACING 'Perform all the walking stuff. END Hopefully if everything came out right, you now have a cat that follows you around! You also know how to setup NPCs and use various library functions to perform common NPC actions (like walking and collision and screen position updating).

Textboxes

My Big Dumb Library comes with a relatively extensive collection of textbox functions. They allow you to show and hide textboxes, display auto-wrapped text, ask for input from the keyboard, and can even display small menus (like the ones you see in RNDLIBTEST). Textbox visuals can be customized right down to an entirely custom drawing routine (for instance, if you want to draw on a sprite instead of on the GRP), and text within textboxes can be customized with a small markup language. You can set colors, sizes, text speed, and insert waits and linebreaks with this markup. Textboxes automatically break up lines and automatically insert scrolls when text is too long to fit into a single box, and even insert short pauses after punctuation to make it feel more like natural dialog. Anyway, enough about all that. To use a textbox, you must first create a textbox parameter dictionary. This is a lot like the SNAKEPATH parameter dictionary: it's going to be a string array that contains the large amount of customization parameters. You can think of it like an "object" if you want: all the textbox functions will work on this "textbox object". You can reuse the same object as many times as you want. Let's create a textbox object to tell the user how to play the game before it starts: DIM TB$[0] 'The dictionary to hold textbox parameters TB$=TBCREATE$() 'Create the textbox. We'll use all defaults GPRIO -30 'Make sure the textbox shows up above all our sprites and stuff. TBSHOW TB$ 'Perform the textbox "show" animation. TBTEXT "Find all the treasure chests! There are 8 in total!", TB$ 'Show some text. Always call "show" first. TBHIDE TB$ 'Put the textbox away Notice all the functions take the TB$ dictionary: they all need to know the parameters required to make the textbox "tick". Also notice the GPRIO -30: the textboxes are drawn purely on the GRP, but the GRP layer defaults to priority 0. Since our cat and player are -10 and -20, they'd display over the textbox, so we set GPRIO to -30. The textbox functions are broken up like this so you have as much control as possible over textboxes. If you don't want to show/text/hide each time a textbox shows up, you can create a simple wrapper function. Something like this maybe: DEF QUICKTB TEXT$,TB$ TBSHOW TB$ TBTEXT TEXT$,TB$ TBHIDE TB$ END This will basically create a "popup" textbox, which may not be exactly what you want each time. Waiting for the show/hide animation each time kinda sucks; sometimes it's nice to leave the textbox on screen and continue to the next dialog without all the animations. Anyway, using the same textbox, let's update the PLPROCESS function from before so that pressing L or R will popup a textbox telling you how many chests are left. We'll use that QUICKTB function we made up just above: IF BUTTON(1) AND (#L OR #R) THEN QUICKTB FORMAT$("%D chests left to find!",CHESTS),TB$ ENDIF We COULD show a textbox every time you pick up a chest, but that's kind of annoying. Textboxes halt the gameplay, so the player wouldn't be able to move until they got rid of the textbox.

Textbox Complete Code

We're going to change a few things: we're not going to display the counter in the top left anymore. It's hard to read anyway, and the textboxes will tell us how many we have left. We're also going to change the first textbox shown to use our little "QUICKTB" function. Finally, we're going to display a special textbox at the end when you win. Here we go: 'Initial setup. You have to EXEC my library (any slot other than 0 will do) ACLS EXEC "PRG3:RNDLIBFULL" XSCREEN 2,100,2 'Use 100 sprites and 2 BG layers (first 2 is screen mode) '--Map Preparation-- VAR I FOR I=0 TO 1 BGSCREEN I,100,100 'Set layer size to 100x100 BGSCALE I,2,2 NEXT BGFILL 0,0,0,99,99,99 'Fill first layer with grass lol (grass is ID 99; first two 99's are the ends) BGFILL 1,0,0,99,99,101 'Fill the collision layer with rocks (will become perimeter) BGFILL 1,1,1,98,98,100 'Fill the inner map with trees (exclude perimeter) '-Path and Chest Generation- VAR SNAKEP$[0] 'The snakepath parameter dictionary SNAKEP$=NEWSNAKEPATH$() 'Create the snakepath parameter dictionary VAR X[0],Y[0] 'The locations for each point (we'll use these later) FOR I=0 TO 15 PUSH X,10+RND(80) 'Random point at least 10 inside the walls PUSH Y,10+RND(80) IF I>0 THEN SNAKEPATH X[I-1],Y[I-1],X[I],Y[I],SNAKEP$,"CLEARPATH" NEXT FOR I=1 TO 15 STEP 2 'Start at 1 so we don't put a treasure right under the poor old lady BGPUT 1,X[I],Y[I],864 'See, the locations were useful NEXT GAMEPREPAREBG 3,1 'Use layers 0 and 1 (bitfield), and layer 1 is the collision layer VAR("3:GAMECLSNSTART")=1 'Any tile with ID 1 and above will trigger collision VAR("3:GAMEPLPROCESS$")="PLPROCESS" 'This is how you'd register the callback so the library calls your function '--Player Preparation-- VAR SP=SPSET(796) 'Define our sprite. Use dynamic sprite ID allocation for ease of use SPSCALE SP,2,2 'Scale sprite up to 2X size SPOFS SP,0,0,-20 'Put player closer to the top GAMEPREPAREPLSIMPLE SP,16*X[0]+8,16*Y[0]+8,1,&HFFFFFFFF GAMESETWALKDATA SP,796,4,8 VAR CSP=SPSET(856) 'Cat is sprite definition 856 SPSCALE CSP,2,2 'Make the cat the same size as the lady (woah) SPOFS SP,0,0,-10 'Put cat below player (but still above BG) GAMEPREPARESP CSP,"CATPROCESS",16*X[0]+8,16*Y[0]+8,2,&HFFFFFFFF 'Put the cat in the same place as the old lady to start. GAMESETWALKDATA CSP,856,4,6 'Old lady's step distance was 8; cat is 6 because legs are shorter lol. '--Textbox Preparation-- DIM TB$[0] 'The dictionary to hold textbox parameters TB$=TBCREATE$() 'Create the textbox. We'll use all defaults GPRIO -30 'Make sure the textbox shows up above all our sprites and stuff. CALL SPRITE 'We haven't run the engine yet, so the sprites are all over the place. Fix it before showing the textbox! QUICKTB "Find all the treasure chests! There are 8 in total!", TB$ VAR CHESTS=8 WHILE TRUE VSYNC CALL SPRITE 'Run all sprite attached functions. Our player is one 'The main loop is the place to put win condition checking. Sprite callbacks can't halt this loop. IF CHESTS<=0 THEN BGMPLAY 5 QUICKTB "You found them all!",TB$ ACLS BREAK ENDIF WEND 'The path clearing function. It HAS to be COMMON so my library can use it COMMON DEF CLEARPATH X,Y BGFILL 1,X,Y,X+1,Y+1,0 END 'Player processing function COMMON DEF PLPROCESS SP,BGX,BGY,X#,Y#,VX#,VY#,FACING,STEPPED 'Player is standing on BGX/BGY. Together with FACING, we figure out which tile we're looking at. IF BUTTON(1) AND #A THEN 'They pressed the A button VAR IX,IY GAMEVECTOR FACING OUT IX,IY 'Convert FACING into unit offsets IX=BGX+IX IY=BGY+IY 'Now IX/IY hold the location the player is facing IF BGGET(1,IX,IY)==864 THEN 'Player clicked A on chest BEEP 3 'Might as well give feedback CHESTS=CHESTS-1 BGPUT 1,IX,IY,0 'Remove chest ENDIF ELSEIF BUTTON(1) AND (#L OR #R) THEN QUICKTB FORMAT$("%D chests left to find!",CHESTS),TB$ ENDIF END COMMON DEF CATPROCESS 'No parameters 'GAMEGETSPXY retrieves the world location of the given sprite. 'CALLIDX is the current sprite (it's a SMILEBASIC thing) 'SP is the player (remember we set it before) VAR CX#,CY#,PLX#,PLY# 'Store the cat and player locations GAMEGETSPXY CALLIDX OUT CX#,CY# GAMEGETSPXY SP OUT PLX#,PLY# 'Now we have the locations of the cat and the player VAR DIST#=DISTANCE(CX#,CY#,PLX#,PLY#) VAR SPD#=MIN(5,DIST#/20) 'The cat's speed will increase as the player gets farther away (up to a point) VAR CVX#,CVY# 'Store the cat's velocity IF DIST#>=16 THEN CHASECALC CX#,CY#,PLX#,PLY#,SPD# OUT CVX#,CVY# 'Figure out cat velocity based on simple chasing AI VAR STEPPED,FACING 'Some variables produced by the walking function (we might use them later) GAMETOSCREEN CALLIDX,1,CX#,CY# 'Update the cat's position on screen (we may have moved; this may not be necessary in the future) GAMESPWALKER CALLIDX,CVX#,CVY#,FALSE OUT CX#,CY#,STEPPED,FACING 'Perform all the walking stuff. END DEF QUICKTB TEXT$,TB$ TBSHOW TB$ TBTEXT TEXT$,TB$ TBHIDE TB$ END A VERY important things to note: look at the place where we setup the textbox for the first time. Before we tell the player "Find 8 chests!", we do CALL SPRITE. This is because the game engine has never been run yet, so the sprites are not yet aligned to the BG and the BG probably isn't in the right place yet. Basically, the engine is "uninitialized"; usually this isn't a problem because the loop starts immediately and everything looks fine. However, now the textbox is inserting a pause before anything happens, and we can see the uninitialized state. If you're going to do anything BEFORE your game loop like this, it's important to either hide the game with something like VISIBLE or to run the game engine at least once so everything looks normal. On a less critical note, I think that kind of feels like a real game now! In the final tutorial, I'll go over textbox tweaks, more advanced terrain generation, and some final finishing touches.
Author
randomous
Updated
Rating
2 votes
Categories
Keywords
  • random
  • engine
  • collision
  • callback
  • npc
  • follow
10 Comment(s) Waffles_X Waffles_X Night Person I like the quiet night and sleep late. Express Yourself Intermediate Programmer I can make programs, but I still have trouble here and there. Programming Strength Touhou Project Is Awesome! I love Touhou Project! Express Yourself I would like to use your engine for a game. This game involves multiple levels, commanding multiple playable characters, and top down rpg gameplay (nut not quite an RPG though). Its kind of like a Fire Emblem crossover w/ The Binding of Isaac. Anyways could your engine handle something like that? randomous randomous Owner Robot Hidden Easter Eggs Second Year My account is over 2 years old Website Drawing I like to draw! Hobbies It can, but you'll have to do quite a lot of that yourself. The engine mostly handles map movement and entities (like the player and npcs). You'll have to handle multiple levels and HOW the players move around and such. I was eventually going to add more stuff to make what you want to do easier, but right now it's mostly just a map collision engine + player movement and automatic animation. Basically... if you make the program at the bottom of this tutorial, that's about all my engine offers right now. Waffles_X Waffles_X Night Person I like the quiet night and sleep late. Express Yourself Intermediate Programmer I can make programs, but I still have trouble here and there. Programming Strength Touhou Project Is Awesome! I love Touhou Project! Express Yourself Okay then. Thanks. I'll see what I can do. Is the most updated version of the engine on your page? randomous randomous Owner Robot Hidden Easter Eggs Second Year My account is over 2 years old Website Drawing I like to draw! Hobbies It should be, yes. I hope it's uh... still up lol. Waffles_X Waffles_X Night Person I like the quiet night and sleep late. Express Yourself Intermediate Programmer I can make programs, but I still have trouble here and there. Programming Strength Touhou Project Is Awesome! I love Touhou Project! Express Yourself Also, can you use custom sprites and music with this engine? Shelly Shelly You can pretty much always use custom sprites with an engine, so I would assume yes. Waffles_X Waffles_X Night Person I like the quiet night and sleep late. Express Yourself Intermediate Programmer I can make programs, but I still have trouble here and there. Programming Strength Touhou Project Is Awesome! I love Touhou Project! Express Yourself Thanks. randomous randomous Owner Robot Hidden Easter Eggs Second Year My account is over 2 years old Website Drawing I like to draw! Hobbies Yeah of course. But the "facing" order of the sprites has to match that of the built-ins if you want to use the walking animation functions. It's something like right down left up or something. Y_ack Y_ack Head Admin I like the part that's more green than black randomous randomous Owner Robot Hidden Easter Eggs Second Year My account is over 2 years old Website Drawing I like to draw! Hobbies I like it more when there's lots of colors :3