Liftoff

A while back I had an idea for a game about launching rockets. Way out of my comfort zone, right? Basically it would be a blend of the engineering and rocket building of Kerbal Space Program, but with a greater emphasis on moment-to-moment skill instead of meticulous planning. I took about a day to prototype the idea, and while I’m not sure if I’m going to do something with it in the future, I did find myself spending a lot more time playing with it than I did building it!

[unfortunately, the Unity Webplayer has been sunsetted, so this demo is no longer available]

Directions: Hold mouse button down to thrust, release to separate stage. Hold down A or D to steer left or right. Reset with Esc.

While the implementation was extremely basic, it was effective at proving the idea and teaching me some interesting things that might be useful later.

It’s an addicting sandbox to play in

The “objective” of the prototype is just to reach as high an altitude as possible, but I couldn’t help but make lots of little games for myself. Try to knock over the launch tower while launching and recover for as high a flight as I could. Try to land gently (very hard since deactivating the rocket at any point jettisons the active stage). Try to skywrite with my breadcrumb trail. Rocket back at the launchpad as fast as possible and blast the objects all over the place. The ability to iterate with next to no load time made screwing up less painful, even fun.

(Approximating) rocket science is pretty easy

As a rocket burns, it uses up its fuel, getting lighter in the process. I simulated this by recalculating the mass of the RocketStage (the primary GameObjects in the simulation), where containerMass is the baseline (approximated by the surface area of the cylinder times a reasonable structural density) and the fuelMass is calculated as the fuel volume times a reasonable fuel density. This all happens right in the property setter, so the calculations only run when the fuel value changes:

public float Fuel
{
    get
    {
        return this.currentFuel;
    }

    set
    {
        // update current fuel
        this.currentFuel = Mathf.Clamp(value, 0f, this.fuelVolume);

        // recalculate fuel mass
        this.fuelMass = this.currentFuel * this.fuelDensity;

        // recalculate rigidbody mass
        this.rigidbody.mass = this.containerMass + this.fuelMass;

        // deactivate if no fuel remains
        if (this.currentFuel <= 0f)
        {
            this.Deactivate();
        }
    }
}

...

internal void Awake()
{
    this.Recalculate();
}

private void Recalculate()
{
    var capsuleCollider = this.GetComponent<CapsuleCollider>();
    var volume = Mathf.PI * capsuleCollider.radius * capsuleCollider.radius * 
                 capsuleCollider.height;
    var scale = this.transform.localScale.x * this.transform.localScale.y * 
                this.transform.localScale.z;

    var surfaceArea = (2f * Mathf.PI * capsuleCollider.radius * 
                      capsuleCollider.height) + (2f * Mathf.PI * 
                      capsuleCollider.radius * capsuleCollider.radius);
    this.containerVolume = surfaceArea * scale;
   
    this.containerMass = this.containerVolume * this.containerDensity;

    this.fuelVolume = volume * scale * 1000f; // 1 m^3 = 1000 liters
    this.Fuel = this.fuelVolume;
}

Speaking of, the RocketStage isn’t in charge of burning any of its own fuel. Instead, that’s the job of the RocketEngine. RocketEngines watch their associated stage for activation, then greedily consume fuel until the player deactivates the stage or it runs out of fuel. The stages are first-class game objects linked together with fixed joints. A RocketController class keeps track of the stages and joints, including which (if any) are currently activated:

RocketController -> 0..n RocketStage <- 0..n RocketEngine

A real simulation would factor in a ton of other stuff I didn’t bother with, but this little toy didn’t have to be completely accurate to start proving out the concept.

A simple CCTV is easy and looks cool

In addition to the chase camera, I wanted to try some more cinematic angles. The ObserverCamera is a hacky CCTV that zooms in (by adjusting the camera FOV) to keep the rocket visible when it’s far downrange. The magic numbers should have been configurable, but they’re based on just a few guesses that mostly worked with little tweaking.

internal void Update()
{
    var distance = this.transform.position - this.target.position;
    var direction = Vector3.Normalize(distance);
    var up = Vector3.Normalize(Vector3.up + direction);
    this.transform.LookAt(this.target, up);

    if (this.zoom)
    {
        this.camera.fieldOfView = Mathf.Clamp(66.7f - (Mathf.Sqrt(distance.magnitude) * 2.0f), 5f, 60f);
    }
}

It’s not a game yet

The core concept of letting off the thrust also jettisoning the stage turned out to be the most compelling part to me, but the reward for burning every drop of fuel in a stage tended to outweigh the extra drag from keeping it attached too long (mostly because I didn’t implement atmospheric drag yet). The game needs a stronger downside to missing the optimal release window, and probably an upside to nailing it (an extra burst of speed?). It also needs more to actually do, like obstacle avoidance or incidental mini-objectives to keep the rocket flying smoothly.

But the exercise of rapidly bringing a concept from idea to min viable is essential to growing as a designer. An idea might seem great and complete in my head, but the flaws and holes in it don’t always become apparent until they’re on the screen. Executing on ideas helps expose what I didn’t know I didn’t know, and in turn makes future ideas more robust by improving my ability to think through them without rose-colored glasses.

Bookmark the permalink.

Comments are closed