Generating Images for a Discord Game
2023-08-20
Est. 7m read
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.
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.
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.
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!
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!
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!
- Check it out: https://discord.gg/pasxV2MTvW
- Website: https://dafarmz.zaaane.com
- 1.0 code: https://github.com/ZaneH/dafarmz-bot