Author: Jeremy Ivanauski
Introduction
When designing Grape Juice City, we wanted the titular city to feel alive and interactive, so we needed a way for the player to affect and be affected by the environment around them. This was implemented into the game as kickable objects and bouncy objects, two types of objects that make the objects around the player feel meaningful.
Kickable objects
We needed a way for the player to interact with the world, but we also wanted this mechanic to be simple to understand, so a casual player could put it to use quickly. This led to the idea of the player character simply kicking everything to interact with them.
One example of a kickable object is the dumpster, which, when kicked, plays and animation and throws out an item to the nearest player. Pretty simple interaction, but how does it work on the technical side?
The way I decided to implement this was to start by setting up a “KickableHitbox” object that is a child object of the player. The script attached to this, KickableRange, keeps track of every game object with a Kickable component that enters its collider. The Kick behavior component in the player has a reference to this KickableHitbox, so when the Kick behavior is executed (triggered by the player,) it goes through each object in the KickableHitbox, finding the kickable object that is closest to the player, and triggers the OnKick function for each Kickable component attached to that object.
The dumpster has its own script that inherits from Kickable, so it overrides the OnKick function to play the animation and spawn the item.
Of course, any other kickable object can override this function for their own behavior that triggers when kicked. This includes crafting stations, which let the player craft an item when kicked, and the players themselves, who get stunned when kicked by another player. This is one of the big advantages of polymorphism; by handling all behavior triggered on kick in one function that can be overridden, we can maintain the open-closed principle. This means it’s easy to add new kickable objects with whatever functionality we need it to, and this functionality can be added without touching any code outside of that object itself.
Bouncy Objects
Another example of interactive objects is bouncy objects, which launch the player high up as once the player lands on them. This came from a desire to promote verticality and platforming in our game, giving players a way to gain a large amount of height quickly. The bouncy object is sort of an inverse of the kickable object when it comes to interactivity, as it’s something that acts upon the player as opposed to something that the player acts upon.
The way I decided to implement this was to create a BouncyObject component that could be attached to any game object that we needed to be bouncy.
Then, I created a Bounce behavior script and attached that to the player. All that script has to do is check if the object the player is currently standing on has the BouncyObject component, and give a player a huge launch upwards. However, there’s a problem with that.
The way I detected what object the player was standing on was by being lazy efficient and tapping into the code someone else already made. This code was originally part of the Move script (and later moved into its own Grounded script), and detected if the player can jump by creating a raycast that checks the area immediately below the player, and set a variable to true if the raycast hit collision to indicated that the player was grounded and therefore could jump. This raycast also conveniently had data on exactly what object it hit, so I slightly rewrote the code to store that object as its own variable that other scripts could get, and the Bounce behavior could just take that object and check if it has the BouncyObject component.
The problem, however, was that the object the player collided with wasn’t always obvious. Shown above is the object hierarchy for the Umbrella object, and the top level object is the one that would ideally have the BouncyObject component. However, the player actually collides with the object called “Collider,” since this is the one with the mesh collider component, and the player never technically colliders with the top level “Umbrella” object.
So, to actually get the player to detect if it collided with a bouncy object, I thought of two solutions. One would be to put the BouncyObject component on the actual collider object. This wouldn’t require any additional code work, but would be annoying to figure out which subobjects of the bouncy object actually had the collider components. Additionally, if an object had multiple colliders that we wanted to have the player bounce off, we would have to attach a BouncyObject component to each of those objects. Because of how messy and unintuitive it would be to create new bouncy objects this way, I decided to not do this.
Instead, I went with the second solution, which was to change how the Bounce behavior tells if the player is on a bouncy object. Now, when the player is standing on ground, the script checks for a BouncyObject component attached to that ground collider or any of its parents, using the function GetComponentInParent<>(). This means that, for an object with the BouncyObject component, it and any of its children will all be bouncy, so we don’t care about which specific object the player is actually colliding with, as long as it’s part of a bouncy object. This might lead to objects unintentionally being bouncy when there a child of a bouncy object, but with good prefab and hierarchy management this shouldn’t be a problem. There’s also a potential performance concern with checking all the parents of the object for that component, although with only one player making these checks on just a few objects, this hopefully shouldn’t have much of an impact.
So, now that we can know when the player lands on a bouncy object, what happens when that happens? Well, after the Bounce behavior finds that the player is on an object with the BouncyObject component, it resets the player’s vertical velocity to 0, then adds a large force to the player’s velocity based on the direction the object is pointing. So, if the object is pointed up, it launches the player directly upwards, but tilting the object will also tilt the direction the player is sent. Additionally, the exact velocity value is based on a variable in the BouncyObject component, allowing bouncy objects to have their own unique bounce force set from a serialized field. This bounce force is also increased if the player is holding jump, and if the player jumps on bouncy objects multiple times in a row Finally, the Bounce script calls the OnBounce function on the BouncyObject, which, similar to the OnKick function, allows bouncy objects to have unique behavior, like cars playing an animation when bounced on.
Conclusion
When I set out to create these interactive objects, I wanted to create systems that would be easily extendable, so our artists could easily create new objects to throw into the world and I could quickly implement whatever functionality was needed on them. I think I succeeded in this goal, as I can take a model created by an artist and implement it with kickable or bouncy functionality. For example, if someone wanted an object to be bouncy, all I would have to do is put the BouncyObject component on it, set the bounce force to something reasonable, and maybe hook up animations to play if applicable. So, I think the systems I’ve detailed above are pretty good at bringing the city to life in an efficient and easily expandable way.
Comments