May 14, 2015

Pony Clicker Postmortem

Never Again...
Pony Clicker was intended to be a fun experiment in designing an HTML5 game. It was developed over a period of about 2 weeks, which was a lot longer than I had anticipated. Normally I build games using low level graphics APIs and highly optimized physics engines, so I wanted to try something that would be simple, where I could rely on HTML5 to do most of the work for me... right?

Wrong. The key thing I learned when designing Pony Clicker was that HTML is evil. If you are making just about anything even remotely interactive in HTML, I strongly recommend using the HTML5 canvas. Everything else will almost inevitably fall over. CSS animations simply aren't going to cut it, and the eccentricities of HTML rendering cause enormous problems with game interfaces. Save yourself the pain and just slap a giant canvas on the screen and render everything to it. Even with the known performance issues with the HTML5 canvas, it will probably still be faster than HTML5 anyway, except now you have much more control over what everything is doing.

I also managed to find a memory leak of sorts in Chrome's DOM renderer. For this reason, Pony Clicker will always take 550 MB of memory while actively using it, until the GC wakes up and actually does its job. The exact details of this are complicated, but the gist of it is that I can create a page that contains no javascript, only a <canvas> element and a <div> element below it with an :active effect, and by clicking on the div element that does absolutely nothing, I can make chrome allocate 20 megs of memory each time. It would be funny if it wasn't so horrifying. I'll write up a seperate blog for that issue.

In terms of game design, Pony Clicker is a more complex version of Cookie Clicker. In Cookie Clicker, each building simply gives you more cookies. That's it. In Pony Clicker, you construct a graph of relationships, and then buy buildings that give you smiles based on how many friends, ponies, or other buildings you have. It's basically Graph Theory Meets Growth Rates: The Game, where each successive building utilizes a function with an ever increasing growth rate. Thus, by the time you reach a limited factorial function, each later building is providing enormous numbers of smiles simply because of the generating function's explosive growth.

Predictably, this made balancing the game difficult. At first, I was excited, because I could start using all that crap I learned in Combinatorics and derive a bunch of equations to balance the game for me based on a few curves that I defined. Inevitably, this did not work. Either the equations were too complex to get reasonable solutions out of, or they simply didn't work, because I was relying heavily on heuristic functions to guess how many buildings would be owned at a given point. I ended up using a combination of functions that allowed me to predict the SPS of any building at any given time, and then used this to define the costs of all the buildings in terms of the cost curve of the friendships. Thus, everything in the game is keyed off the friendship cost curve, which can be modeled by a recurrence relation:
\[F_{n+1} = r F_n\]where r is the curve value (Pony Clicker uses a value of 1.6, because that's close to the golden ratio and it seemed to work nicely). This is a trivial linear recurrence relation, so we can get a closed form solution out of it:
\[F_n = F_0 r^n \]The same kind of curve is used for just about everything else in the game, including cost curves of the buildings. Cookie Clicker uses the same curve for all it's buildings, where $$r = 1.15$$. This stops working for Pony Clicker because the later buildings provide ever increasing amounts of smiles, by design. To compensate, the cost curve is much more aggressive for the later buildings. Initial costs were supposed to be chosen based on the number of friendships that would be bought at the time of the initial building price, but this kind of fell apart. However, it was still useful to key the cost off of the friendship curve, so I ended up with a really weird initial price array: [4,12,30,35,45,45,45,51,51,100]

To host the game, I used GitHub's pages, which means it's all being hosted out of a gh-pages branch in the github repo. I commit changes to the master branch, then do a git pull into the gh-pages branch and then git push to sync it with the master branch. So far this has required me to have the deployment branch checked out - if you know of a way to merge changes from one branch into another without cloning that branch, I'd love to know about it. Also, if you want to contribute to the game, with upgrade suggestions, more witty news articles, art, etc., feel free to either send me a message, or just submit a pull request on GitHub!

I used Visual Studio Code to write this project. It's intended for web development, and in fact is just a webpage being rendered in a container. It actually works pretty well for small projects, but once a file exceeds around 800-1000 lines of javascript, it's auto-complete is too busy compiling running around with its hair on fire to be of any real use, which is a shame. I'm kind of wondering why it was implemented as a webpage though. Perhaps to make a point? If they wanted to make the point "web apps are still slow as crap when they need to do anything nontrivial", I guess they succeeded.

2 comments:

  1. Hi, I would like to know why your game isn't working for me. I have a Windows 7 computer, and a few security settings that might be affecting it, I just don't know which. When you can, email me at aljonviray@gmail.com when you can. Thank you.

    ReplyDelete
  2. >intended to be a fun experiment
    When I first saw the game, I thought you must have been exceedingly interested to build such a thing based on my own experience doing graphics-heavy stuff in web browsers for work (I stepped through your code in F12).

    Thank you for writing up your experience; I'm really curious to see your writeup of the Chrome issue. My most recent Chrome-only graphics issue was a map that would flash rapidly when zooming or panning even though the mammoth Javascript library was supposed to abstract away the browser differences (solved by hard coding a variable).

    ReplyDelete