Repurposing Old Systems
It's always a little sad to see good code slip into obscurity as gameplay changes and mechanics drift from their original goals. During our lengthy exploration into RedFrame's core gameplay, a lot of our ideas reached a fairly playable state, only to be discarded once we embarked on our next prototype. But all is not lost; by diligently using version control (SVN - that's for another post) we've retained a complete history of our creative and technical output. I'll often pursue old systems to remind myself of previous ideas that may become relevant again some day.
One such forgotten system was an object carrying mechanic that I developed about a year ago. The system offered some neat affordances for both the player and the game designer: the designer could mark an object as "portable", then mark valid drop locations on surfaces. At runtime, when the player approached the portable object it would highlight to indicate interactivity, then they could click the mouse to pull the object into their hand. There could never be a case where the player could permanently lose the object, such as by dropping it behind a couch, because the designer would not have designated that area as a valid drop location.
It was a great system, but it became a solution looking for a problem. We quickly ran into an interaction problem common to most adventure games: pixel hunt. It's a major failure of design when the player is compelled to click aimlessly throughout an environment in an attempt to discover interactive items. The issue is bad enough on static screens in point-and-click adventures, and a full real-time 3d environment only magnifies the problem. The system had to be abandoned - it just didn't work in the game.
Fast forward a year. Just last week we realized we had a related problem: our core gameplay had been reduced to interaction with 2d planes (we'll talk more about this in future posts) and we'd lost the feeling of actively participating in this dense world we'd created. To avoid spoilers I won't reveal the precise nature of the solution we're currently exploring, but it turns out that my object pickup system was perfectly suited for the job.
At this point I have a known problem, and I have code that can potentially solve it... but now how much of this code is actually usable? Luckily, the code came into our new project without any errors.
In general, it's not uncommon for older code to have to be thrown away simply because it can't easily interoperate with new systems. When it becomes more work to fix old code than to write new code, you can become trapped by constant churn that will bog down even a small project. To mitigate this, I try to structure my code in a very decoupled way.
Rather than writing my pickup and drop code against an existing player controller, I instead included two generic entrypoints into the system:
PortableObject FindNearestPortableObject (Transform trans, float maxDistance, float viewAngle)
This method searches for PortableObjects within a view frustum implied by the position and rotation of a given Transform object with a given angle-of-view. I chose to require a Transform rather than a Camera component since it can't be guaranteed that our solution requires us to render a camera view. It's generally best to require only the most generic parameters necessary to perform a desired operation. By artificially restricting the use of a method by requiring unnecessarily specific parameters, we harm future code re-use without adding any value.
DropNode FindNearestUnusedNode (Transform trans, float maxDistance, float viewAngle)
On the surface, this method is effectively identical to FindNearestPortableObjectToTransform. Internally, it uses an entirely different search algorithm. This is a case where conceptually similar tasks should require a similar invocation. This serves two purposes:
- Technical - It's possible to swap two methods without re-working existing parameters, changing resulting behavior without having to track down new input data. This increases productivity while reducing the occurrence of bugs.
- Psychological - By using consistent parameters across multiple methods, the programmer's cognitive load is significantly reduced. When it's easier to grasp how a system works, and it requires less brain power to implement additional pieces of that system, the code is much more likely to be used by those who discover it.
Lastly, the system includes a PickupController. This is a general manager script that manages picking up and dropping one object at a time, using the main camera as input. PickupController has no dependencies outside of the scripts belonging to its own system – it assumes nothing about the scene's GameObject hierarchy aside from the existence of a camera, and doesn't require any particular setup of the GameObject that it is attached to. It simply scans for PortableObjects to grab and DropNodes to place them into. By making the fewest possible assumptions, it's able to be included in just about any project without having to be modified.
Writing re-usable code can certainly not be easy, but I've found that its long-term benefits tend to outweigh the cost of minimally increased development time. Once you're comfortable with writing reusable code you'll find that your earlier work will pay off again and again, making you more productive by obviating the need to repetitively solve the same problems.