🐮 "Is it my cow?" Elm game Postmortem - Part 2 - A cow without legs
This is the continuation post of game postmortem blog post series you can find the complete list of this series posts below:
"Is it my cow?" Elm game postmortem series:
- The idea and core problem
- The cow without legs (currently reading)
- Animation
- coming soon...
🐮 A cow without legs
As we have cow patches already, it's time to spread them over the cow body. Namely, we need to create some graphics of a cow. As I've mentioned before I am not an artist, although I like to try my skills in other disciplines from time to time.
How I become fond of vector graphics as a kid
The interface of the legendary Adobe Photoshop software never appealed to me, GIMP was a bit friendlier (and cheaper 😋) for me but still not something that I would stick to when creating the graphics from the ground up. I've never been good at raster graphics, although I must admit that I have some good memories of creating pixel-art-like 32x32 Windows icons in Microangelo 😊.
Nevertheless, on days when my adventure with my first PC started I got Microsoft Office someday. The Power Point had become my favourite part of this software because it allowed to create some fancy style animations which 90s software is known for. But beside that feature, it allowed to create some simple vector shapes that could be mixed together to create more complex graphics. I've figured out that I could take some inspiration from existing pictures and create some shapes step by step with vectors. These days, I use the LibreOffice for that, but instead of the Impress (which is a PowerPoint alternative), I use LibreOffice Draw.
Designing in LibreOffice Draw
I guess many designers will say that LibreOffice Draw's UI is cumbersome, but for some reason it fits good for me and beside some annoying behaviours like snapping the elements wrongly as I move them, I must admit that it is still the way of vector graphics creation that I mastered most. Also, it has the feature that we'll need for our webbrowser-based game - SVG export. Unfortunately, the SVG that LibreOffice produces are far from being optimal. I think the good analogy for it is the HTML code that Microsoft FrontPage produced - by some people called HTML soup in it's "glory" days. Nonetheless, this issue can be easily resolved by using some SVG optimization tools. I must admit that I haven't used such tools during the development of my game, but I believe that "Premature optimization is the root of all evil" phrase sometimes also applies to disciplines other than programming. In other words with a limited spare time that I had for gamedeving, I had to cut the corners.
I've decided to find some pictures of a cartoony-styled cow, so I can recreate a cow with simple oval shapes. I've composed a cow head mainly from a bunch of ellipses:
After that, I've put another ellipses that would demonstrate my vision of complete game cow with body:
But in the end, I've not exported these parts to the SVG file. I've decided that my game engine will build the rest of the body of a cow through code. So, cow "view" function operates on these elements:
- Cow head from an SVG file
- Ellipse which will contain cow patches clipped
- 4 ellipses that represent cow legs
To achieve the 2nd one, there is SVG ClipPath element that enables to build path that clips overflowing graphics.
Again, because time that I could invest into my game was limited, I've decided to limit the first development iterations to cow head and body with black patches only, so cow legs got postponed to later iterations:
🐄 Placing the cows
To make game main scene more attractive, I've decided to place the cows (player's cow and misleading ones) randomly. Random 2D position could be easily generated through Random.pair generator like this:
type alias Position =
( Float, Float )
randomPosition : Random.Generator Position
randomPosition =
Random.pair
(Random.float 0 500)
(Random.float 0 500)
Then, Random.list
generator can be used with it together to generate positions for multiple cows.
There is one issue with that approach though:
As you can clearly see on above's picture, dummy random position generator like this one cannot guarantee that the sequence of the random positions won't overlap with each other.
Imperative-like solution
The first thought that came into my mind was how I could easily fix this in imperative programming language. Specifically, I would just make sure after calling function like Math.random()
two times (for each position coordinate) in JS, that the resulting coordinates aren't too close to some of the coordinates generated previously, if so, I would do another function call (x2 because of each coordinate) again until the result is satisfactory.
The problem with that simple idea firstly is that in Elm, this would not look so easy to implement this. That's due to the fact that to get the random value in Elm, you have to return a Random.generate
command in your update
function, and this in turn forces you to add some distinct message that handles the result of random value generation. That's the way the Elm deals with side effects where random value generation clearly belongs too. Adding the second pass of random value generation would need a second Random.generate
command which could get update
function polluted a bit more than it should be and also, the main disadvantage of that approach is the fact that commands aren't composing well. Just take a look how nicely you can compose Random generators together, you can take for instance random number generator, and wrap it with Random.list
generator to generate a list of numbers. This composition is very much alike how JSON Decoders work in Elm. But Commands are harder to tame in Elm, because your code splits to at least two places:
- You have to create your command before it's returned from the
update
function - You have to handle the message with the result in some of the
update
function branches
Sure, we can imagine that we support the random generation logic in the same update branch so two of above points would become one, although this does not make it an easy task to turn a command that generates a particular random position into a command that generates a list of such positions.
Snapping the cows to the grid
Nonetheless, I ditched this approach from the start due to some other simple fact. It just does not look right to make my program to perform unnecessary looping or recursion. Instead, I've just figured out that I can just divide the level into a 2D grid, where each cell has the dimensions of a cow. This simplifies the problem a lot, because during the random positions generation process, we can eliminate one of the available cells from such grid so on next step we can guarantee that every cell can be picked only once. Argh, dang it... I am still thinking in imperative programming manner... Let's think how to use the 2D grid idea in a functional programming manner!
Let's just represent our 2D grid as a list of tuples of x and y axis indexes:
type alias Grid =
List ( Int, Int )
So, we need some constructor for our Grid
type data structure. The API that I wished at
that time was a function that takes two Int arguments:
grid2D : Int -> Int -> Grid
grid2D cols rows =
[] -- TODO: implementation
Before implementing the proper function body though, how about trying elm-test
to
write some unit tests that can help us to write the correct implementation? Here's our first test suite:
module GridTest exposing (..)
import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer, intRange, tuple)
import Grid
import Test exposing (..)
fuzzGridSize =
tuple ( intRange 0 10, intRange 0 10 )
suite : Test
suite =
describe "Grid module"
[ describe "2D grid"
[ test "Creates valid 3x2 grid" <|
\_ ->
Expect.equal
[ ( 0, 0 )
, ( 1, 0 )
, ( 2, 0 )
, ( 0, 1 )
, ( 1, 1 )
, ( 2, 1 )
]
(Grid.grid2D 3 2)
, fuzz fuzzGridSize "Created grid is a list with length = cols * rows" <|
\( cols, rows ) ->
Expect.equal (cols * rows)
(List.length (Grid.grid2D cols rows))
]
]
And here's not so beautiful but test-passing implementation:
grid2D : Int -> Int -> Grid
grid2D cols rows =
List.range 0 (rows - 1)
|> List.concatMap (\y -> List.range 0 (cols - 1) |> List.map (\x -> ( x, y )))
Now, having the grid data structure, we can create some random generator that will help us in collision-free cow positioning. If we have a list of cells already, how to pick a random number of them with repetition-free guarantee? Not optimal but simplest solution that came into my mind is just to shuffle the list, and just take n
first elements of such shuffled list. Luckily, there is a nice shuffle
function in elm-community/random-extra
package. So, the implementation of our function is pretty straightforward:
pickRandom2DGridCells : Int -> Grid -> Random.Generator (List ( Int, Int ))
pickRandom2DGridCells n grid =
Random.List.shuffle grid
|> Random.map (List.take n)
The last element that is missing in our random cow position generator is just converting a list of cell (x, y) indexes into a SVG position. But this is an easy task, just map the first element (x) of our tuple with ((*) cowWidth)
-like function and the second (y) element with ((*) cowHeight)
.
Side note: if you have enough motivation to review the final code of my game, you may notice
that Grid
module differs a bit from the code I presented here. That's due to the fact that instead of using the Elm's builtin Lists I wanted to try some lists data structure variation that guarantees lists that have at least one element. To be concrete I've tried mgold/elm-nonempty-list
package. This required a bit more of a code to be added, although the idea is still more or less the same.
"Find your cow" scene
When I got a mechanism to position cows nicely, I thought it will be nice to give my cows some nice and green scenery (designed by me in LibreOffice Draw again):
After putting it all together in my view
function I came across some problem during testing my game on my personal iPhone SE (version 2016) screen which is nice device for edge case tests on iOS platform by the way. Although mobile platform support was not intended as crucial feature in my game, I was curious how it presents on mobile. Unfortunately, I've found out that game was not playable due to the fact that some cows were placed outside of a screen. But SVG has some ace up his sleeve that can fix this easily - preserveAspectRatio
attribute. By putting all of the SVG elements into one SVG container and setting it's preserveAspectRatio
to xMidYMin meet
I could make the "Find your cow" scene layout responsible so the meadow fits into the screen width but it's location is always constrained to the bottom of the screen at the same time:
Although, as you've probably noticed, the cows on the above screen have legs already (and some other features like lives and score by the way). This is because the "aspect ratio" fix was applied after adding legs. In terms of these blog series however, I've decided that legs themselves will be a good topic when we get more into the animations stuff.
⏩ To be continued...
This is the second post of the series of the blog posts where I tell a story with some details how I developed my game. Next time we'll give our cows some legs and take deep dive into animation problems.
Stay tuned!