Published on

Generating Images for a Discord Game

Authors

Recently I built a Discord game called DaFarmz. It's a game where you can plant seeds, grow crops, and sell them for coins. It's a pretty simple game, but I wanted to write a post on the process because I think it's pretty cool.

The Curiosity

If you've read this post, you'll know that it starts with curiosity! I was curious about how to generate images after using the well-known OwO Bot.

OwO Bot

An example of OwO Bot's battle system. The dynamic images piqued my interest.

Initial Research

After I saw the battle images, I was thinking how bland they are. It's amazing that they're generated on the fly, but what if there was a game that had more interesting images?

Before I looked into image generation, I wanted to think about what open-source tools I could use to generate the most interesting images. My first thought was Godot! If I could generate the images in Godot, I could add physics, animation, particles, and more! How cool would that be?

When experimenting with a new solution, I always create a "PoC" or "Proof of Concept." This is a small project that tests the core of the idea.

Godot

In this case, I rendered a red rectangle onto a Godot canvas and exported it as a .png. The first few attempts were unsuccessful, turns out there isn't a documented way to check if the first frame has been drawn. I ended up using a timer to wait 1 second before exporting the image. This was a good PoC, but it wasn't what I was hoping for.

Even then, I tried to push forward, the next thing I needed to check is if this would work on a server. So I ran the binary with --headless and... it crashed.

This wasn't too surprising, it makes sense that Godot would need a display to render to. From previous experience, I imagine that there are workarounds, but this is where I stopped. I needed something that makes sense out of the box. It was then that I decided to keep it simple and find a way to just layer images on top of eachother.

ImageMagick

I've heard of ImageMagick and it's the first thing that came to mind. However, I've used it on the CLI and it is not fun. I wanted to find something that made it easy to generate images programatically.

Headless Chromium

The next thing that came to mind was Puppeteer or Playwright. I've used them to generate images from HTML/CSS in the past. The advantage to this method is that you can use CSS for styling and dynamic layouts. The disadvantage is that it requires a full instance of Chromium to generate the images. This didn't sound like an ideal solution either.

Jimp

This was a new one for me. At first glance I was hesitant once I saw their logo was a crayon. But after reading through some snippets, it's exactly what I had in mind! It's a simple library that allows you to resize images, add text, layer images, and more with simple function calls.

Just like before, I went to create a PoC. I created a simple JavaScript app to layer an image on top of another. It worked! The code was clear and I was ready to move forward.

Here's a small sample from the PoC:

const generateBaseImage = async () => {
  const layer1 = await jimp.read('./js_image/images/layer-1.png')
  const layer2 = await jimp.read('./js_image/images/layer-2.png')

  return layer1.composite(layer2, 0, 0)
}

There's even a Jimp add-on for creating GIFs if I want to add animations later on. For server performance, I'll stick to static images for now.

Grid Math

The formula for placing items is pretty simple. y=offset+(32/2i)y = offset + (32/2 * i)

Elixir

Elixir has been growing on me over the last year. I've seen someone describe it as "a way to write the application you care about, without having to fix every wart." This resonates with me and it's a perfect use-case for the Discord bot.

On top of that, Elixir has Ecto which will make it easy to store game data.

What I didn't know is how to use my JavaScript PoC in Elixir. I searched around for about 5 minutes before stumbling on NodeJS. It let's you run JavaScript code from Elixir! This was perfect, and essentially the final piece of the puzzle.

# pass game state to app.js
{:ok, path} =
    NodeJS.call("app", [
        Jason.encode!(%{"discord_user_id" => user.discord_id, "state" => js_input})
    ])

# send plot image
Api.create_message(channel_id,
    content: ...,
    file: %{
        name: "plot.png",
        body: File.read!("#{path}")
    }
)

The Game

Now that I had the tools, I needed to build the game. I had only two requirements:

  • It needs to be simple
  • It needs to be visually appealing

Building the game out, I started with Nostrum. It's an Elixir library for interacting with Discord's API. I had never used it, but it looked much easier than Discord.js, which I have used.

Here's a snippet from handling the daf command:

def handle_event({:MESSAGE_CREATE, msg, _ws_state}) do
  case msg.content |> String.downcase() do
    "daf" ->
      case Users.get_by_discord_id(to_string(msg.author.id)) do
        # Check if user exists
        {:ok, user} ->
          {:ok, _} = Responses.send_user(msg.channel_id, user)

        _ ->
          # Send setup message otherwise
          {:ok, _} = Responses.send_setup_message(msg.channel_id)
      end
...

Once I had it talking to Discord, I started building some concept art. Having spent time on itch.io I've seen a lot of game assets. One that I've always wanted to use is Cup Nooble's Sprout Lands pack.

Concept Art

First draft while trying to figure out the layout. Once again, Cup Nooble made the assets.

It's very cute and has farming assets which should be simple to implement. I started with designing the plot and added an apple_seed for testing. I added images for the lifecycle of the apple tree and after a few hours it was kind of working!

Apple Seed

Notice how the cost is an int of 6000. This translates to 60.00 in-game. Storing money as an int is a common practice to avoid floating point errors.

Next I added the watering mechanic, and energy bars. I then added a shop to buy more seeds. I manually added shop items to the database, that seems to be the best way to do it while testing.

After that, I added the harvesting mechanic, the ability to sell items, the ability to recharge your energy, etc. It was coming together! After a few more hours I had a demo ready to go. You could plant seeds, water them, harvest them, sell them, and buy more seeds. Best of all, you can use daf plot to see your plot!

Plot

I lightly patched it up over the following days to make improvements where I saw fit. I bought the premium Sprout Lands pack to add even more crops. I created a Discord server, hosted the bot on a server, and now it's public! Others can invite it to their own server with this link.

Retro

I'm really happy with how it turned out. I was surprised by how quickly Elixir allowed me to build a game. This is pretty new territory for me so it was nice to have things "just work."

If I am to build a chat bot like this again, I'll certainly reach for Elixir if it's an option. One thing I would change is to use MongoDB (or another document store) instead of PostgreSQL. I think it would be easier to change the schema as the game evolves.

The game definitely needs more content to justify monetization. But I'd really like to see the first few users before getting into that. Until then I'm expecting to pay ~$4/mo. to keep it running at its current usage.

Benchmark

After about a week, I decided to benchmark the image generation code. Here is the benchmark code.

The benchmark is running 100 requests to layer 1 plant on top of the plot:

Name                     ips        average  deviation         median         99th %
generate_image          0.41         2.42 s     ±1.00%         2.43 s         2.44 s

And I decided to benchmark again with 10 plants being layered on top, again 100 requests:

Name                     ips        average  deviation         median         99th %
generate_image          0.23         4.42 s     ±0.99%         4.42 s         4.46 s

This does not include the DB calls or the Discord API calls, so there's additional latency to consider.

The Future

Who knows what's next for this. I just created it for fun. It gave me experience with generating images on the fly which is something I haven't really done like this before. I also got to use Elixir in a fun way, and I got to use Cup Nooble's assets which I've been dying to use! That checks off a lot of boxes for me.

I think it'd be cool to see plots interact somehow, or maybe have a leveling system to unlock more seeds. I'll give it more thought. Until then, you can play it on Discord!