Ghost Images in Against the Dying of the Light

Light snow crunches under your boots. The cold snap happened so suddenly, it's a miracle you had winter gear in the back of the car.
Hi! I'm @Celtican. I'm the programmer that helped make Against the Dying of the Light, a story-driven adventure survival game. The above text is the first line of dialogue the player sees when playing the game, but I'd like to talk about the dialogue another time. Mind if I talk about a peculiar but very basic visual effect in our game? Thanks!
You can call it different things, "Ghost Image," "Shadow Trail," "Bleeding Image," etc., but I'm talking about the blue trail behind the player when they walk. Look:

I came up with the idea pretty early in development, before any assets were made. I don't make any non-technical art (I leave that to @Gardenovena), so I tend to think about how I can make things interesting with code alone. Usually through tweening, shaders, other other such effects. In this case, I created the ghost image effect with the intention to emphasize lethargy. Unfortunately this kind of effect is also associated with super speed, but the effect was artistically interesting enough that we kept it in the final build.
You might be able to tell how the effect was made already. The character itself is animated with Dragonbones, a skeleton animation software. @Gardenovena only used Dragonbones a few times before LD50, this walk animation was the first animation she ever completed. We took her animation and exported it at 8 FPS (we thought it would look nicer that way), and then every frame I create a copy of that frame in the player's position. The newly created "GhostImage" is then colored navy blue (the primary color of the game), and fades out over the course of 0.25 seconds.
You can see it more clearly when I ~~destroy~~ hide the world:

By the way, I only noticed when making this gif, but apparently the legs are missing a frame of animation. No one (at least not us) noticed while playing because the legs are often hidden by grass, and you usually aren't looking at his legs in the first place. Oops!
To programmatically create the GhostImages, I made a CreateGhostImage() function that fires every time the frame changes. I don't detect a frame change, I leave that to Godot (I'm using Godot by the way) ala signals. I setup a signal (basically an event) that fires on the "frame_changed" event, and it connects directly to the CreateGhostImage() function. The CreateGhostImage() function itself is relatively small:
```cs // Player.cs
// various relevant variables private String _lastAnimation = "stand"; private int _lastFrame = 0; private float _lastScaleX = 1;
public void CreateGhostImage() { if (Animator.Animation == "walk" && (Animator.Frame == 0 || Animator.Frame == 4)) { // every time the character takes a step walkSound.RandomPlay(); // Really, this code should be in another function. // But, you know Ludum Dare, we love spaghetti code here } // Since CreateGhostImage() is called after Animator.Frame is set to // a new value, we need to fetch the old value. I opted to do this // simply by keeping the previous frame information in memory Texture frame = Animator.Frames.GetFrame(lastAnimation, _lastFrame); _lastAnimation = Animator.Animation; _lastFrame = Animator.Frame;
// I hate loading the resource every few frames. I was probably so much
// in crunch mode that I didn't even realize I did this.
Node2D ghostImage = GD.Load<PackedScene>("res://scenes/GhostImage.tscn").Instance<Node2D>();
// GhostImage doesn't have any code, so its behavior is handeled
// all in Player.cs. This includes its position, scale, and tweening.
// In production code, I would probably put this code in a static
// GhostImage.Instantiate() or similar.
ghostImage.Position = Position;
Sprite s = ghostImage.GetNode<Sprite>("Sprite");
s.Texture = frame;
s.Scale = new Vector2(s.Scale.x * _lastScaleX, s.Scale.y);
// This is used when I flip the image when the player transitions from
// looking left to looking right, or visa versa (I didn't make a gif
// that shows this effect, but it's pretty basic)
_lastScaleX = AnimatorHolder.Scale.x;
// I specifically want to put the GhostImage behind the Player in update order.
// If I did not do this, the GhostImage would render above the Player while
// the player was moving left and right (because their order would not change when
// their Y is the same)
GetParent().AddChild(ghostImage);
GetParent().MoveChild(ghostImage, 0);
// Lastly some tweening. Just to make it fade out nicely. I'm tweening
// the GhostImage's color from its default (#482d3375) to invisible
// linearly over 0.25 seconds. Godot has a useful Tween node class
Tween tween = ghostImage.GetNode<Tween>("Tween");
tween.InterpolateProperty(s, "modulate", s.Modulate, new Color(s.Modulate.r, s.Modulate.g, s.Modulate.b, 0), 0.25f);
tween.Start();
// And... it's done. Honestly not too much code for a nice effect.
} ```
Additionally, I accidentally made the GhostImage slightly offset from the Player. This resulted in the GhostImage showing up when it is created even while the player is standing still. This doesn't happen normally because the "stand" animation's FPS is set to 0, but it does happen when I manually create a ghost image. I create a ghost image whenever the player chops a tree, and builds the bridge. It's pretty nice actually, there's a tiny bit of feedback even though we didn't have time to make the chop/interact animations. You can see this a little bit in the second gif on this post.
... Actually, we did make a chop animation. We made it in the last 20 minutes of the jam. That's correct, I was pressured to code this animation AND test it AND submit a build to itchio in less than 15 minutes. The crunch rush actually caused some major build bugs that prevented the game from running, needless to say the animation didn't make it. Which is a shame, really, because it demonstrated the ghost image effect much better than the standard walk animation. This is because the chop animation has much more movement, and so the trail is easier to see, even though the player is standing still while the animation is running. Here's the animation in the dev environment by the way:

This animation, made in 10 or so minutes, caused me so much hell. Particularly the delay for the chop sfx and tree disappearing was what made the crunch so difficult. Anyways, I'm still sad we couldn't put it in. Actually, that was the only asset that didn't make it in, which is impressive since our previous jams had many assets that didn't make it in the final build. But they were content-driven games, I suppose.
Anyways, this effect was super easy to add and was made probably in around 15 minutes or less. Most video game polish is done without any assets at all! I typically find that "game feel" is found through a combination of tweening, particles, shaders, and a bit of sfx. Of course, wonderful art really adds to the experience as well. This game would be nothing without @Gardenovena's pretty pictures.
I've been reading a book about game feel, I admit I haven't come anywhere close to finishing it though (I, uh, have more important things to be reading). Game feel is super important in video games, and is pretty easy to add (I tried to link some YouTube videos but the formatter bugged a bit. See "The Art of Screenshake" and "Juice it or lose it" on YouTube). I probably could have done more polish on my game, but I'm pretty content with the amount of polish it has.
I hope you learned something, and that this was insightful? Maybe? I've been writing a lot about the game in its comment section. Either way, feel free to link your own game so I can check it out and rate it. See you next time!
Rate Against the Dying of the Light here:https://ldjam.com/events/ludum-dare/50/against-the-dying-of-the-light






