Game1 W4/4 progress
This post is midway through the last week I have to work on the game in a platforming genre. And it’s a bit of a shame because currently, I’m having a blast - now that the physics and camera are in place the game already feels like a playable thing. So I decided to boost this feel and make some graphics so the world won’t look too boring to traverse.
I haven’t decided on what the world be like yet, but I’m running out of time, and also, I’m not that experienced in pixel art. A few posts ago I wrote how I tried to do pixel art on the phone by using a stylus, and it worked pretty well for me. So I took out the app again and started thinking about how would I use the colors that are still available to me.
I’ve briefly considered changing the palette too. The default TIC-80 palette is a bit weird for my taste, and it doesn’t include some colors that I’d like to have, e.g. brown. I don’t know how, but every time I see some pixel art that uses this particular palette I swear it has brown, but upon closer inspection it’s an illusion caused by interplay with the other colors. Unable to find a new palette that works, and also that doesn’t make every other sprite I drew up to this point use some weird colors, I gave up for now. Maybe I’ll just look at what colors I don’t use at all or use too infrequently and replace them with the colors I need for my purposes.
For now, I drew some grass on stone type of terrain (again, no brown for dirt!), and some construction site-type red bars:
Note, these are tiles, e.g. what goes on the map, but there are some things that are not part of the map, for example, the coin, or the slime enemy. I’ll explain why they’re here shortly.
And here are the sprites:
As you can see, I’m storing these sprites mostly in rows, and tall sprites, like the player character look naturally in the editor. Except for the ones in the third and fourth rows first half - these are horizontal sprites, but I still store them this way, because of how I do animations. This reminds me that I haven’t explained anything regarding how my game works! I guess I was too busy implementing it and didn’t have any spare time to write a proper post.
So the animations are implemented in terms of sprites, obviously, but the way I set up them is a bit weird.
And I know, I definitively could save some space and re-use some sprites, because clearly there are doubles or even triples of the same sprite.
But it was easier this way, so I did it anyway.
I have this function, called sprites
that takes an id of the first sprite, the last sprite, and an optional loop?
parameter:
(fn sprites [from to loop?]
(let [sprites (fcollect [id from (or to from)] id)]
(doto sprites
(tset :n (length sprites))
(tset :loop? loop?))))
The result of this function is a table, that simply holds a range of IDs for all the sprites in the animation.
Does the n
key hold the total count of sprites, and loop?
, well, we’ll get to it in a moment.
The other component of animation is how I choose when to change the frame. I’m sure there’s a different, and probably more robust way of doing this, but what I’ve settled on is this function:
(fn next-frame [obj dt]
(let [dt (+ dt (or obj.last-draw-time 0))]
(if (>= dt 60)
(let [sprites (obj:sprites)
n (m/floor (/ dt 60))]
(set obj.last-draw-time 0)
(if (= obj.frame sprites.n)
(case sprites.loop?
n (if (= :number (type n))
(set obj.frame n)
(set obj.frame 1)))
(set obj.frame
(clamp 1 (+ n obj.frame) (length sprites)))))
(set obj.last-draw-time dt))))
As you can see, this function receives an object obj
and a delta time dt
.
The object stores its own draw time, and when the sum of dt
and this time is greater than 60
I change the frame.
The loop?
field is used in a special way - when the next frame exceeds the total sprite count, we check if the animation should loop.
If the field is true
the animation loops from the first frame.
However, if the field is a number, we restart the animation from that number.
This way the animation can have a set of frames that act as preparation or transition, and then loop only the necessary part.
Here’s how I draw coins:
(local coin-sprites (sprites 330 335 :loop))
(local coin-collect-sprites (sprites 346 351))
(fn draw-coin [{: x : y : frame &as self} {:x cam-x :y cam-y}]
(match (self:sprites)
coin-sprites
(let [s (or (. coin-sprites frame) 511)]
(spr s (+ cam-x -120 x) (+ cam-y -64 y) 0 1)
(next-frame self (/ dt 2)))
coin-collect-sprites
(let [s (or (. coin-collect-sprites frame) 511)]
(spr s (+ cam-x -120 x) (+ cam-y -64 y) 0 1)
(next-frame self dt)
(when (= frame collect-sprites.n)
(tset coins self nil)))))
There are some wonky numbers in here to adjust to the center of the screen and camera position in the world, but the code should be straightforward.
The (self:sprites)
call simply returns the object’s current set of sprites, which I change in other functions depending on the situation.
E.g. if the coin was collected, the object’s sprite is changed to the coin-collect-sprites
which doesn’t loop, and the last sprite of that animation is invisible.
The coin itself is deleted from the world once the last frame
of the animation was played out.
This is the basis for all animation in the game.
Each object has it’s :draw
method, and each such method calls to next-frame
to understand what to display next time.
Interaction with entities is done similarly - each entity has an :interact
method that decides what to do when a collision is detected.
For example, for the slime enemy, the method accepts the y
normal of the collision, and if it is not negative 1, which means that the collision happened from the side or from below, the player is damaged.
Otherwise, the slime sprite set is replaced with the stomp animation, and the object is deleted from the world:
By the way, I haven’t talked about how I structure the world yet. You may know, that TIC has an inbuilt map editor:
This is basically a level designer for me because I’ve implemented a small function that upon loading the cart walks through each tile on the map and populates the world based on it.
I still use map
for drawing the level, but before that, all entity tiles are removed from the map, like in the image above coins and slimes, and the player’s head, which acts as a spawn point.
This way I can simply put objects in the map editor, and re-run the game, getting an updated world, complete with collectibles, power-ups, enemies, etc.:
Here I’m obviously testing things out, but the game feels complete enough to actually start designing some actual levels. I’m running out of time though, so perhaps I’ll just make a single course to complete, and that’s it. But it’s already more than I’ve anticipated at the beginning!
I’ve also started working on the menu, but there’s nothing to show yet.
From now, I’ll probably stop doing any in-game stuff, and concentrate on the level itself and I also need some audio to play when the player collects coins, takes damage, jumps, and so on. Maybe I’ll even include some music if I’ll manage to compose something not too annoying to listen to.