johan helsing.studio

dice another day - jam devlog

Dice Another Day is a game I made for GMTK Game Jam 2022. It’s a puzzle game where dice are missing pips and you roll them around and try assemble the right pattern. In this post, I’ll go through its development, both tech and design-wise.

Coming up with an idea

As usual, the idea didn’t form immediately. At first I wasn’t even sure if I was going to join the jam, but when the jam started at 19:00, I had a bit of time, so I did some brainstorming, here are the results:

I had an idea about assembling a die that I liked. By picking up individual pips from a tightly constrained board (sort of like sokoban). It would have walls you couldn’t move outside, and once you picked up a pip it would be used up, so you’d have to think hard about where to put each pip to complete the missing patterns.

Starting with art style (again)

I’d decided to not do any programming that day, so I thought that regardless of whether I actually ended up making a game, it would be fun to do some 3d modelling and make a die. This is what I ended up with:

And since I now had a model, I thought it’d be fun to put it into Unity to see what it looked like.

Making felt

When I’d added the die in Unity and played around with the material a bit, I needed something to put the die on so it didn’t just float in the air.

I thought I’d try to go for some casino table-like fabric, called felt. I tried searching polyhaven.com, which is my go to resource for pbr textures, but coulnd’t find anything that resembling it. After searching a bit, I found cgbookcase.com, which apparently is another great source of CC-0 textures. They had a jeans texture that I thought looked kind of cool.

Of course, it was gray. First, I tried just tinting the color in unity, but it ended up too dark, so I lightened it using level adjustments in Krita instead. That looked decent enough, but it was kind of too regular:

I didn’t like the way the stripes all aligned, so I used a trick sometimes dubbed “mozaic tiling”. This divides the plane up in multiple cells with a bit wacky borders, and rotates the texture at random within each cell. The gist of it is you add some random noise to the uv coordinates, then run it through a floor and a hash function which you can then use to rotate the uv’s within each cell.

The result is much less noticable patterns:

Then I added a bit of depth-of-field blurring to give it more of a close-up photography look. And some screen-space ambient occlusion to make the dice look more grounded.

And that concluded day one.

Turning it into a game

At this point, I had a look that I was really happy with, but no actual game. This made me put quite a bit of pressure on myself to actually deliver something. I had one and a half day to go, but I was a bit anxious as to whether my initial idea was maybe a bit too ambitious.

Rolling in a 2d grid world

While I was thinking of ways to simplify it, I started adding things that I deinitely knew I wanted, which was mainly getting the die to roll around on a grid.

With puzzles, my usual approach is to write all my game logic separate from from Unity gameobjects, in simple regular C# classes. I have a single class, WorldState, that contains all the things that may change in the game, but the actual class itself is immutable (read-only) as far as C# allows me to. I implement “moves” as a function that simply takes one WorldState and an input direction and returns a new WorldState. WorldState is serializable, which makes it super easy to implement undo: I simply store a list of past states, and pop them off the list to undo.

On the Unity side of things, I need to sync with the current WorldState. The way I usually go about this, is to just use easing functions (usually SmoothDamp) to smoothly put the objects where they should be. This quickly got me to a stage where I had a die that was sliding around on a 2d plane (the ground), however it was not really rolling…

I wrote a bandaid fix for this that made the 3d die rotate “realistically” in the direction it was different from the gameplay world.

This made it immediately look right, but it also meant there was no real concept of rotation in the gameplay world, which was a bit annoying when I later wanted to add gamplay that needed to both be aware of which face was currently down, and also the orientation of the face that was facing down.

In the end, I solved this by letting the die have six different faces, and depending on the direction pressed, the patterns were simply moved to new faces, and sometimes rotated, for instance here are my move left/right functions

public DieSides RotatedRight() => new DieSides
{
    bot = right,
    right = DieSide.Rotate180(top),
    top = DieSide.Rotate180(left),
    left = bot,
    front = DieSide.RotateCcw(front),
    back = DieSide.RotateCw(back),
};

public DieSides RotatedLeft() => new DieSides
{
    bot = left,
    left = DieSide.Rotate180(top),
    top = DieSide.Rotate180(right),
    right = bot,
    front = DieSide.RotateCw(front),
    back = DieSide.RotateCcw(back),
};

Getting this synced back to unity was a bit of a pain though. It probably took me a few hours to write, and ended up so ugly I’m actually too embarassed to post here.

Adding the pips

The die sides themselves are simply bitfields, which meant each side fits in 9 bits, so for simplicity, I just used an int for it. I then simply enabled or disabled game objects for each pip based on the bits set, like this:

[ExecuteAlways]
public class HoleGrid : MonoBehaviour
{
    [SerializeField] int pips;
    [SerializeField] GameObject[] holes;

    public int Pips
    {
        get => pips;
        set => pips = value;
    }

    void Update()
    {
        for (var i = 0; i < holes.Length; ++i)
        {
            holes[i].SetActive((pips & (1 << i)) != 0);
        }
    }

    void SetStandardPattern(int numPips) => pips = DieSides.patterns[numPips][0];

    // buttons to make level editing easier
    [Button] void One() => SetStandardPattern(1);
    [Button] void Two() => SetStandardPattern(2);
    [Button] void Three() => SetStandardPattern(3);
    [Button] void Four() => SetStandardPattern(4);
    [Button] void Five() => SetStandardPattern(5);
    [Button] void Six() => SetStandardPattern(6);
}

public class DieSides
{
    public static readonly Dictionary<int, int[]> patterns = new Dictionary<int, int[]>{
        { 1, new int[] { 16 }},
        { 2, new int[] { 68, 257 }},
        { 3, new int[] { 84, 273 }},
        { 4, new int[] { 325 }},
        { 5, new int[] { 341 }},
        { 6, new int[] { 365, 455 }},
    };
}

So to actually rotate the patterns, I had to move some bits around, which was slightly trickier… this is what I ended up with:

public static int RotateCw(int g)
{
    // probably cleaner ways to do this, but jam
    var tl = (g & 1) >> 0;
    var tm = (g & 2) >> 1;
    var tr = (g & 4) >> 2;
    var ml = (g & 8) >> 3;
    var mm = (g & 16) >> 4;
    var mr = (g & 32) >> 5;
    var bl = (g & 64) >> 6;
    var bm = (g & 128) >> 7;
    var br = (g & 256) >> 8;

    return bl * 1 + ml * 2 + tl * 4
        + bm * 8 + mm * 16 + tm * 32
        + br * 64 + mr * 128 + tr * 256;
}

// could be done directly, but jam
public static int Rotate180(int g) => RotateCw(RotateCw(g));

By the end of the second day, I had a die rolling around and when it landed on a pattern tile, it would add the pips from that pattern. Since it’s all bitfields, the code for this was really simple:

public DieSides InkBottom(int ink) => new DieSides
{
    bot = bot |= ink,
    back = back,
    top = top,
    front = front,
    left = left,
    right = right
};

Simplifying the idea

I now had one day to go, but still no game, just a dice roller/stamper toy. It meant I had to start killing darlings.

Originally, I had inteded that pips would be “picked” up and removed from the ground once you rolled over them, however, I decided I didn’t actually need that behavior, the game would work just fine without it, so I didn’t prioritize it.

I also postponed, and in the end dropped, the idea of having wall tiles. Puzzle-wise, I could achieve the same thing by just placing “illegal” patterns on the ground. That would immediately make an invalid die (fail state), which is virtually blocking the player the same way walls would have.

I then wrote the win condition, added level loading and made most of my levels in under an hour. I then got my wife to playtest and arranged the levels in roughly the order she found them most difficult.

“Polish”

That meant I now had what could be called a “working game”… and around 4 hours to go. However there still were some really rough edges.

I still had:

  1. missing in-game instructions
  2. bugs with undo
  3. no sound effects
  4. I hadn’t tested it on webgl
  5. no itch.io page
  6. no win animation, it would just cut to the next level
  7. no ambience sound
  8. I’d only tested the game with one person

I decided to fix the remaining issues in the order you see above. I prioritized sound effects pretty heavily as I think it can make otherwise dead gameplay seem a lot more alive. I’m pretty happy with how good rolling the die rolling sounds, compared to how little time I had to make it.

Blocked by painfully slow WebGL builds

Building for WebGL turned out to be a pain (as it always is). I did the jam on my (old) laptop this time, and that meant the initial build took over 40 minutes. During this time, I was essentially blocked from working on the game. This would have been a good time to work on sound effects, but I’d already done that. I used some of this time to work on the game’s itch page and instructions, but these are really things that I could have done after the deadline, so it was essentially time wasted.

Also, as it turned out, performance on WebGL was initially horrible, especially on HiDPI laptops with poor graphics hardware (like mine and also the cheaper/lighter macs). I’ve run into this before, though, so I knew what to do. I had some code that checked for ridiculous screen resolutions, and if detected, it would halve the render scale (so the game is rendered at a quarter of the resolution, then scaled up). I also quickly wrote some code that would simply disable all post-processing if a certain key was pressed.

I was quite lucky that all this worked on the first try, which meant I still had time to make a really simple win “animation”. I just slapped on a rigidbody to the die and added a random upwards impulse to it, which I think worked quite well.

Things that didn’t make the cut

From my prioritized “polish” list above, the only thing that didn’t make it was ambience and additional playtesting. In other words, I was really efficient the last couple of hours of the jam, which I’m pretty poud of.

More playtesting

Obviously, more playtesting would have made the game a lot better. It definitely has some issues with pacing and leaving people confused.

I think a lot of people didn’t understand the concept and were left confused in the first two levels. At least the data I gathered showed that around 50% of players didn't make it through the first level, and 50% of those didn't make it through level two. So either they thought the game was ridiculously boring, or they didn't get it at all... or some combination, but I'm hoping that it was mostly due to confusion.

More varied props

I think it would have added some freshness to the levels if the environment was a bit more varied. In every level it was the same stack of three dice. It would have been quite easy for me to just vary the setup, and change the color of some of the dice.

I also think some stacks of poker chips, and perhaps a card house would have been cool.

Reset level button

When you mess up really early, pressing undo a lot is kind of tedious, especially since the movement is throttled by the die rotation speed. This is something several people have already mentioned in the jam comments.

This is kind of a bummer, as I already had the code in place to restart a level, I just didn’t have time to hook it up to a button.

A more exciting “several dice” at once level

I already had the code in place to move several die at once, and I have one level with two dice. It would have been really easy to do a “fun” level where you move a whole bunch of them.

Hints when in an invalid state

I also wanted hints to show up when you’d ruined the die and suggest undoing. I think this would have left a lot of players less confused. A lot of people seem to believe they’ve made a die, but they really have two of one side, or perhaps some malformed pattern on one of the sides that aren’t visible.

Hints when stuck for too long

I also think level specific hints would have been nice. Or perhaps a way to skip levels.

Outdated Mac build and no Linux build

Since building and switching was so excuciatingly slow, I prioritized getting the WebGL build performant and bug free. I was planning to build for windows, mac and linux (in that order) if I had time, but even with the extended deadlines (itch.io had server trouble due to the jam being so huge) I did not have time make a mac build after I added the performance fixes and win animation, and I had no time to make a linux build at all.

This is kind of ironic, since I developed the game entirely on Linux.

What worked well

  1. I was really good at recycling old code this time
  2. I think I managed my time and prioritized pretty well
  3. I think it was a good decision to use Unity over Bevy this jam. Doing these sort of post-processing and the lack of a real editor would probably have made it very annoying to work with.
  4. cgbookcase.com is a really great resource for CC-0 pbr textures
  5. mozaic tiling is a great way to get rid of patterns when tiling textures
  6. It was really inspiring that the game looked good from the very start, it kept me motivated through frustrating hours of bug fixing and when axing features

What I should have done differently

Again, starting with the art-style meant I had little time to playtest and iterate on gameplay and levels. It would have been really useful to get insights from more players early on, but perhaps I’d just be uninspired and quit the jam during the annoying bug fixing period and not delivered anything at all… It’s hard to say, but it’s certainly a difficult trade-off.

Conclusion

Overall, I’m really happy with this jam.

I'm considering continuing the work on this one if I can come up with some idea that will make it scale.

Comments

Loading comments...