Saturday, March 19, 2016

Production Post 10 : End Game

We're about two weeks away from release and it's coming down to the wire. The project is dripping with my blood, sweat, and tears, and I have to just keep working harder and harder in order to deliver that release build in a state I will be satisfied with. I'll have so much time to sleep when I'm dead, so why bother sleeping now?

I would put the main menu at about 60% functionally complete, and 30% polished. The pause menu still breaks minor stuff and doesn't look good. The dragon movement is only mostly finished, but not in a state we can release with. The levels are looking a lot better, but the ghosts need new, better AI, the ocean level needs a lot of visual polish, the splash and boulder crush particle effects are severely lacking, and the wind obstacle particle effect needs some minor tweaking.

One huge area of our game we are missing entirely is prompts to tell the player how to play. We need to make this as soon as humanly possible, one shot, polished, ready for release. We don't have time to iterate on this.

The game needs a LOT more feedback to inform the player what exactly is going on. We need to polish the credits menu. It's too late to make that difficulty curve tool for Zac, so we're going to need to sit down and figure out a decent way to work around that limitation.

Will and I are still pushing heavily for the infinite play mode, which the systems are almost entirely already made for, but I'm going to put that at 50% probability of materializing.

We need to make it so the split orbs get destroyed (with feedback) when they get hit by an obstacle. The Overworld gate animations still aren't finished. We still need to add chapters, A LOT of chapters if we don't have time to make the infinite play mode.

We have a document on how we might optimize the game, but we need to sit down and optimize it. The framerate on mobile is still very not great. Load times are verging on atrocious, with no indications that loading is occurring.

There's still so much to do: these next two weeks are going to be critical. Compounding this with other classes, which are also coming down to the wire, and you get eight very stressed out, very overworked developers. But that's half the fun! We've all worked too hard on this to not come out with a polished, bug-free, beautiful game, and I know we will be able to do it. But wow are these two weeks ever going to push us to our limits.

Production Post 9 : Inspiration Outside of Games

I have a snake. Her name is Mona. She is a sweet babe and I love her. Here is a picture of her on my head.


As it turns out, she moves a lot like how our dragon moves! Watching her move actually inspired a lot of how the dragon moves.


This isn't her, I don't have a gif of her slithering around, so I found this other snake that looks like her on the internet! But notice how a turn will start with her head, and propagate down through the tail. In code, this ended up being the node system of motion I implemented. A turn starts with the head, and the nodes follow, but not directly, the motion of the head. It kind of smooths out down the tail:


In the past, the nodes would follow the path of the head exactly. It didn't feel right, it felt more like controlling a train than a dragon. It's hard to put into words the tiny details of how a snake moves, but as the turn propagates down the tail it tends to sort of straighten out.

When making games, I think most of the best inspirations come from anything that isn't a game. Observing the world around, and other forms of media, you makes great, original material for systems, functionalities, and games in general.

I haven't ever seen a game entirely made out of shadows, which means at the very least it's a relatively unique style of game. It also gave way to a series of really interesting effects and mechanics that work well with a game made of shadows. We got the idea to do this through a series of thoughts, the major breakthrough being shadow puppetry.

Self-sacrifice and protection are themes rarely seen in games: to arrive at these we thought a lot about motherhood. Given that none of us are mothers, this was a difficult thing to do accurately. But we keep this idea in mind as we develop the game, and I think it shows through.

But I think what we've done here has been important in shaping how I look at games: it's easy to think that all the good game ideas are already taken, but to create an original, interesting game I think you have to step outside of the gaming world and draw inspiration from anything else.

Production Post 8 : Particle Effect Vacations

I love implementing particle effects. So much. It relaxes me: after working on core systems, tearing my hair out over complex algorithms, and handling the responsibilities that come with being the lead programmer: making particle effects is practically a vacation. I just kind of zone out and mess around with sliders, values, and curves until things look pretty. I like making things in our game look pretty.

So I'm going to post a few gifs of some particle effects I've made, and briefly talk about them!

This was a pretty quick one to make, I wish I could have made the actual line thinner as it expanded, but that isn't how basic scaling works. The time it would take to make that work is more time than I'm willing to spend on something like that. Maybe when we get to pure polishing, but there are more pressing things to attend to at the moment.


There are two particle effects here: one when a boulder crumbles and another, more subtle fog/mist effect under the mountain. The boulder smashing was the first particle effect in the game, and I'm not particlary (heh) happy with it. The mist I made earlier today: a lot of stuff is happening with that but my favorite part is that as it rises, the second or third phase of transparency brings it slowly to zero alpha, which gives the illusion that it dissipates as it rises into the atmosphere.


I set the background dandelion effect to render as shadows on the same layer as the background, which I think looks really nice. If you notice, the dandelion tumbleweed thing actually effects the background dandelions! I attached a wind zone to effect the background particles, and a sphere trigger collider to impart a force onto the orbs. The dandelion ball obstacles aren't finished yet, but they look alright. To get them to fly around in a circle, I had to give it an x force over time as a sine wave, and a y force over time as a cosine wave. Circles! Everything is circles!

Ah Unity default particles. Maybe I'll change the texture of these to one of the smoke particles Shannon made, but then again... meh. I think this one looks pretty good. I do need to resize this: the ghosts used to be tiny, shitty hands until Amanda made them ghosts. So the effect would execute, and I destroyed the hand a couple split seconds after the effect started, which made it look like a smoke bomb kind of magic poof. You can see the ghosts disappear while the effect plays, and yeah me no like.

I'm looking through the game and, uh, I'm not seeing any more. That's really disappointing for me. I need to make more particle effects. Luckily, most of my tasks right now are particle effects now that the Overworld is done. Next week I'm thinking I'm going to spend a lot of time on these. It's going to be a good week :)

Production Post 7 : A Brave New Overworld

A couple days ago I rewrote how the Overworld functions! It extensively changes how the Overworld interacts with DataManager, and how a player navigates. Here's a clip:


All the tutorials are on a big wheel now! With zooming in and zooming out, proper level progression (persistent progression saving included with DataManager), and.... drumroll.... chapter progression! This is a pretty large step forward in development, and puts us significantly closer to a complete build ready for release. Also, it allows Zac to design and implement new chapters seamlessly.

In this post, I'll talk about how it works currently, and how it can be improved.

First off, there are six states the Overworld exists in:


This object-state design pattern is proving to be invaluable. There are different inputs that need to be processed and followed unique events for ZOOMED_IN and ZOOMED_OUT.

ZOOMED_IN recognizes, for now, a single tap to begin zooming out. If the tap was on a node; however, this zoom out event is overridden by the level entering.

ZOOMED_OUT recognizes a single tap which switches over to the ZOOMING_IN state. It also recognizes swipes left and right, and tells the chapter wheel to begin rotating in the correct direction. This instantiates either the next, or the previous (with exception handling for if we are on the first or last chapters and whatnot) chapter, rotated 90 degrees, and positioned either left-ward or right-ward from the wheel axis.

That instantiation makes the new chapter a child of the wheel, allowing me to simply rotate the wheel with the chapters following, without having to worry about moving the chapters at all.

As far as I know, this all works really well! No bugs have come up and everything seems fine from the player's point of view. But, I think the system is flawed and a bit hard to follow for a few reasons. I made this switch about 3/4ths of the way through writing this: I went from reading everything from DataManager to storing localized instances of data. The chapters are destroyed and then instantiated, which means the indices of the List of chapters does not line up with DM's list of chapters (the List of Lists of level IDs I talked about a couple posts ago). If I can change it to sync up perfectly, and hide inactive chapters on the wheel rather than destroy them, it would make saving level progressions a bit more sensical. I have a feeling that this convolution will lead to some minor bugs in the next week, so when those pop up (inevitably when Zac starts adding chapters), I'll redo a bit of how this class works. Until then, who really cares?

Production Post 6 : Data!

A couple weeks ago, the team and I went to implement some little thing. I don't recall what this thing was, but it did uncover a whole slew of issues that urgently needed resolving. We traced the issue back to GameManager, but to fix it we would have to change a lot about how we are saving and loading data. The team and I worked for a few hours to try and hack what was already there together, but it just wasn't working and we all went home at around 1AM. Side note, walking out of the lab with a fundamentally broken game is, like, really disheartening.

So I went home, got some sleep, and death-marched back to campus the next day hell bent on making this work.

Step one was to make an entirely new class: DataManager. DataManager would store all the World and Level data the game needed to operate. All objects, manager, and scripts that needed this data would run through this class. This was no small task: at the time, there was practically no management of this data: there were places that stored data that meant different things than the other places. The code was littered with pseudo saving and loading that sort of did the actual save file writing and loading pretty randomly. The previous SaveLoadManager was this tacked on thing that the game sometimes wrote to, but most of the time did not. It was a nightmare, but not unconquerable.

First thing I did was write make two structs. These structs went through a few iterations, especially with how we managed the completed nodes, but the final structs became:


There are two instances of these structs in DataManager, and EVERYWHERE in the code the previously referenced random half-assed versions of these could now just call a long series of getters and setters defined in DataManager to modify these values. Another neat thing I did to further make DataManager a pleasant black box: in all the setter functions, in addition to just setting the data, I also make a call to the Save() function. This means that absolutely nothing in the code has to deal with saving and loading, DataManager would just do it.

Previously, there were a few calls throughout the code to the Load() function. This now served absolutely no purpose, since the most recent data is always accessible from DataManager: no file-reading required. So, the ONLY place in the code where Load() is called is in DataManager's Awake() function, which only calls once at the start of the application. Beautiful!

Which brings me to the next big change: replacing the non-functioning, platform-dependent data file with Unity's built in PlayerPrefs. PlayerPrefs is essentially a managed data file with a simple Dictionary-style lookup system. It was exactly what we needed. The Save function is as follows:































Simple enough, and totally under the hood, taken care of, and nobody needs to worry about saving or loading. A huge improvement already!

But, PlayerPrefs can only save three data types: ints, floats, and strings (along with nice Dictionary-style lookup keys). So, as you may have noticed, the only data that isn't one of those data types is the List of Lists of integers which stores the CompletedNodes for each chapter. Saving this as a string was the obvious choice, and two functions had to be written to handle converting this list of lists of ints into a string: one to convert from string to List<List<int>>, and one to convert from List<List<int>> to string.

The main part of this was adding two unique identified characters: a '$' to indicate the end of a saved node ID, and a ';' to indicate the end of a chapter's list of node IDs.

Below, I'll post the (nicely commented, as usually) functions:

List<List<int>> to string for saving

string to List<List<int>> for loading
Reasonably elegant, I think, but ya know it doesn't really matter because, yet again, it's completely a black box. The only thing myself and my lovely programming team have to use from DataManager are the getters and setters to access and set data.

In addition to writing DataManager, I had to scroll through every single script in the entire project and make it all work with DataManager. It made a lot of stuff more elegant, and, more importantly, functional, which is something it was previously lacking. The DataManager opens a lot of doors there were previously either closed or really difficult and time-consuming to find.

And that's pretty much all of how DataManager works. So far, it's made the OverWorld possible, and sped up development time on pretty much anything that has to know about World or Level data.

Friday, March 18, 2016

Production Post 5 : I'M DROWNING IN RATIOS

The ocean level. The fucking ocean level. My god, my god, what have I done. There was a point where the ocean level worked fine and nobody really cared that you couldn't change the overall level time. The world was rose-colored and life was worth living. That all changed when we wanted to be able to change the overall level time.

The level functions along the same principles of object-state design I talked about in my last post, with the following managing enum:


The FLOOD states indicate that the waves are rising, and the DRAIN states indicate that the waves are falling. Below is a gif of the ocean level with the level time sped up:


The short rises and falls are what the MINI refers to, while the overall flood or drain are what the FLOOD and DRAIN refer to. I needed to relate the overall level time with the time for each of the above states. So, I made a public variable to decide what percentage of the level the FLOOD would take relative to the DRAIN (see how the drain takes longer than the flood? That was a major pain to accomplish in units of mini rises and falls). Then, I made another public variable to relate the distance a mini fall would cover compared to a mini rise. These variables were made with the FLOOD in mind, because a DRAIN is just an anti-FLOOD in the code (with a few caveats).

I'll post this here, but you shouldn't read it really. It demonstrates the ridiculousness required just to get the correct variables to make this thing work (that was half the battle, I'll admit, and it was very uphill).




































That was fun to work out. All of those variables were absolutely necessary, though. I had to relate every single type of interpolation time and distance to each other using the rations, and make sure the entire monstrosity took exactly <LEVEL_TIME> seconds. It was a labor of love, and totally worth it, because the ocean level actually works with the rest of the game.

I'm not going to post any more code, as it is pretty repetitive going down, but because of that initialize function, the level runs off of just four major state functions: HandleFloodMiniRise(), HandleFloodMiniFall(), HandleDrainMiniRise(), and HandleDrainMiniFall().

I remember writing all this crap: I made a lot of hand gestures to think about these ratios and how they fit into the level as a whole. shudder

Production Post 4 : "Object State" Architecture

The way I've been writing a lot of my scripts has been similar to what a finite state machine accomplishes. A finite state machine usually implies a somewhat complex AI to make those state changing decisions, but for my purposes I've been running these object states on timers.

The problem is that objects often need to execute a series of functions, in order, when timers are completed. This is especially true when we need to interpolate through a series of positions, rotations, scales, colors, etc for polish. I've seen this problem solved by ugly, ugly boolean checks that increase in complexity and decrease in readability very quickly as more states are required. I've been taking advantage of enums to resolve this.

This is a bit hard to explain, but it's actually a very simple concept, and one that has made my code very readable and easy to add and remove functionalities. So, to explain, I'll use the Wind Level as an example:

The important functionality of the wind level is that an invisible wind represented in code by a changing force vector blows the orbs off screen. It was easy enough to get the base wind blowing, but in order to feel like wind I couldn't just have a constant force vector for the wind, I needed to add turbulence. I accomplished this with a series of interpolations that execute in-order. This is the enum that I used:


Then, in the update loop, I have a switch statement that calls the proper functions for the current "turbulence" (object) state (mTS):


Really simple, highly readable, and yes all of these comments were in there to begin with I didn't just write them for this post. So, the HandleNone() function increments a timer, and when the timer runs out, it resets the timer for the BUILDUP state and changes mTS to BUILDUP. Super easy to follow, and with no nasty boolean checks.

I like to use this kind of structure even if I only have two states (which one would typically default to a boolean variable). This is because whenever there are two states, odds are we'll want to add another one later for some polish reason. In this way, the code becomes very modifiable. Maybe we want to make it so the orbs all shoot off a windy particle effect, but only during the COASTING state. Without having to really search through any code, we can accomplish this very easily by going into the HandleCoasting() function and writing some code there!

In short, using enums to manage object states is a really good idea; it makes your code readable, modifiable, and gives you a mini finite state machine to work with in the future!