In any large game project it’s generally a very good idea to keep major systems decoupled. By avoiding direct method calls, shop it’s possible to build systems that communicate with one another without necessarily having to know about each other. This has one major benefits: a large system can be built in complete isolation, disinfection designed to send calls to components that either may not yet exist or may be replaced with another system in the future. Such a decoupled approach makes it relatively easy to modify systems without destabilizing other dependent systems.
While RedFrame’s mechanics are fairly simple in comparison to many first-person games, there are still a few complex systems that must communicate effectively and may change significantly throughout development. Specifically, our puzzles consist of a handful of components that must communicate with each other, but to allow us to rapidly prototype our puzzle ideas, these components must remain largely autonomous and only weakly connect with each o.
Method Calls vs Messaging
– SendMessage built-in
– Manual reflection-based calls
– David Koontz system
– C# Messenger Extended
– Flashbang’s system
C# includes a language feature for defining events. Since events can be foreign to many Unity developers, it’s worth briefly describing what they’re all about.
____ delegates, events, how they’re called
The most decoupled way to call a method is through Mono’s Reflection class, System.Reflection. With reflection it is possible to discover all methods, both public and private, that a given object contains. These methods may be invoked by their string name without requiring a hard reference to the method from within your code. If the method whose name you’re trying to call does not exist in the object that you’ve targeted, you can write your code to simply ignore the invocation.
This is, in fact, what Unity does internally whenever you include one of MonoBehaviour’s built-in events in your scripts, such as Awake and Update. Unity will reflect over all of the MonoBehaviour instances in the current scene, find these special methods by name, and invoke them at the appropriate time.
Writing your own reflection code can be a bit hairy, so Unity kindly includes a few utility methods that do it for you.
SendMessage & BroadcastMessage
Each script that you create inherits two useful reflection-based methods from MonoBehaviour: SendMessage and BroadcastMessage. SendMessage may be used to invoke a method by name on any object, including itself. BroadcastMethod goes one step further and invokes the method on any of the target objects children, too. Of SendMessage is a scalpel, BroadcastMessage is a sledgehammer.
With both of these tools at your disposal, you can safely invoke methods on another object that may or may not contain the method you expect.
There is one glaring issue with this approach in relation to messaging: you first must know the object on which to invoke the method!
Custom Messaging 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, remedy 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.