I set out to participate in this quarter’s #FC_JAM a few weeks ago. I scoped out the various fantasy consoles available, trying to find something lightweight and fun to use. Most fantasy consoles are written in C and accept Lua scripts, but I found that TIC-80 released JavaScript support and that instantly convinced me. Since I’ve been studying so much JavaScript, getting to stick with that language was the appealing choice. The console itself is all-inclusive, with a terminal, sound, sprite, and map editors, and with artificial restrictions of 240x136 pixels display, 16 color palette, 256 8x8 color sprites, and 4 channel sound.
I fired it up, made a new file, and I was in business.
Starting Out
Starting out, I played with text effects and rendering to see what kind of interface I could make. I quickly found that the default fonts are very large, and it would look too cluttered to use them. Instead, I used their various properties to animate them and create strong title text.
I started out kind of cheeky, but it was pretty easy to get a fun aesthetic going on the screen. The console is intuitive, and the built-in functions abstract away the annoying parts of trying to emulate this look.
The library worked as expected in JavaScript, but having just learned a lot of ES6, I wish I could’ve used it here. I thought of transpiling my code with Babel but it wasn’t worth it. Due to the memory constraints, brevity is key and there isn’t room or need to use those new features. That said, the PRO version of TIC-80 allows up to 8 memory banks so you could really cook with fire if you wanted to.
While I wanted to dedicate myself fully to making a game for the jam, I also wanted to take my time and get comfortable with the tool. Simultaneously, I was pretty busy at work with some development projects, so I was experiencing some burnout in the evenings. That said, I focused on trying to execute two things to frame a simple game:
- a state management system
- a platformer system
State Management
I was unsure what to call my starting project, but after drawing some sigil-like glyphs in the sprite editor, I created a menu screen:
I was able to draw any size centered text with a helper function. It allows for y-offset so you can move the text up or down while keeping it centered at the correct size. I overlaid two similar colors on the palette to give it depth.
var printCenter = function(string, color, size, yOffset, xOffset) {
print(string,Math.floor((240-(print(string,0,-50, color, undefined, size))-(xOffset || 0))/2),Math.floor((136-6)/2)-(yOffset || 0), color, undefined, size);
}
State management was simple to configure. Game states were represented as objects with functions:
var states = null;var Menu = {
init : function() {...},
update : function() {...},
render : function() {...}
}
};
function TIC() {
if (state != null) {
state.update();
state.render();
}
}
The TIC() function is the game loop of the console’s engine. It fires 60 times per second to produce the framerate of your game. Since we’re using the same interface for each game state, we can partition code for each state’s logic in update() and handle displaying the results with render(). I created a Menu state to start.
I wrote a init() function in my game as well, to set up the game state prior to running the game loop. I used this function to start the time and set the first game state.
function init() {
t = 0;
time = 0;
score = 0;
Menu.init();
}
init();
Platformer
The working title for my game was Priestess, because I drew some cultish sprites and a red-robed woman who became my main testing avatar for the game. The main game state where the platformer would happen was called Town because the initial idea was to draw out a town. However, I quickly realized I didn’t have the sprite freedom to achieve anything dramatic in this short span of time.
Starting with the init() function, I set the camera’s coordinates to be the player’s location minus half the height and width of the screen. I also set this object the same way in the update() function every time it fires. Later in the render() function this will allow me to draw the tiled map around my character, keeping the camera centered like a traditional platformer. I also set the player’s location to the starting place on the map. Using the CELLconstant, I code clearly in cell sizes, which are 8 pixels.
var Town = {
init : function() {
state = Town;
camera = {
x : player.x-120,
y: player.y-68
};
player.x = 8*CELL;
player.y = 9*CELL;
}
The update() function would performed the keybindings to move and animate the sprite with helper functions, eventually. After that, this function applied gravity to the map to produce a basic platformer.
The green tiles are sprite drawn onto the map with the built-in map editor. The white cube represents the player. In the corner you can see some of my debugging numbers. The cardinal directions indicated which corners of the player were touching solid tiles. Gravity carries the player down too slowly in this iteration, it would later be improved. You can also see the start of a basic “Game Over” screen that fires when something happens (in this case, when the player walks off screen).
Checking if the corners of the player are clear is done with a helper function:
var openCell = function(x,y) {
switch (mget(x/CELL, y/CELL)) {
case 1: return false;
case 2: return false;
case 6: return false;
default: return true;
}
if (mget(x/CELL, y/CELL) == 0)
return true;
else
return false;
}
I used a switch to check the ID of the sprite against whether it’s solid or not, to determine if the cell is open or closed. This function is used to check the appropriate corners of the player when they press directional keys to move.
With the basics down, I started to play around with the render() function and the built-in functions of the library to manipulate the screen. One of my favorite experiments created a glitch effect that sequentially corrupted the sprites, maps, and sounds actively during play.
To draw keep the game centered around the player, and draw the map in relation to the player, I used a camera object with x and y properties to track the player. I set the coordinates of this object to be the player’s location minus half of the width and the height of the screen. This allowed me to draw the map around the player and keep the screen centered.
map(0,0,240, 136, -camera.x, -camera.y, 0);
spr(player.sprite,player.x-camera.x,player.y-camera.y-CELL,0,undefined,player.flip,undefined,player.spr_w,player.spr_h);
Embellishments
Once I got to this point, I started to get a lot of ideas about where to take this basic platformer framework. What I had so far was very sparse, so I added a background image and scaled it up to render behind the player. By translating it across the screen by relational coordinates of the player, I produced a low-bit parallax effect.
spr(136,-15-(player.x/6),-34-(player.y/8),0,5,undefined,undefined,8,8);
I ended up taking this one step further to add clouds in the background, drawn from sprites and moved at various speeds across the screen, to give more depth to the parallax. Given the low resolution, the result is surprisingly nice.
spr(104, 30-(player.x/8), 10-(player.y/100), 0, 2, undefined, undefined, 2, 2);
spr(106, 140+(player.x/4), 11-(player.y/100), 0, 4, undefined, undefined, 2, 2);
spr(108, 100+(player.x/5), 20-(player.y/100), 0, 3, undefined, undefined, 2, 2);
spr(110, 30-(player.x/2), 0-(player.y/100), 0, 6, undefined, undefined, 2, 2);
As you can see, I also added animations to the character that can can stack relatively to each other. The priestess has a natural chest breathing moving, and when moving from side to side she also executes a movement animation. The shoulders rise and fall in her animation in the same timing as standing still. I was able to achieve this with a helper function that abstracted animation and made it possible for any object on the map.
var doAnimation = function(object){
var spr_default = object.spr_default;
var spr = object.sprite;
var frameCount = object.frame_count;
var frame = Math.floor(Math.random() * frameCount);
if ((spr + frame > spr_default + frameCount) && (t % 60 == 0)){
return object.sprite = spr_default;
}
if (player.is_falling == true) {
return object.sprite = 179;
}
else{
object.sprite = (spr + frame);
return frame = null;
}
}
#FC_JAM
The jam itself was hosted by egodorichev