In the last post, one of the major clean up items we briefly mentioned was that we updated and fixed throwing of objects. In this post, we’re going into detail on how that was accomplished. Heads up — this is more of a technical post, but we will try to simplify the technical parts.
In Violet, we had an issue where if we were standing next to a wall and throwing an object, the object would “get stuck” in the wall. This is demonstrated by this amazing illustration below.
The solution to the problem was not very hard to solve for. However, we originally overengineered a solution by examining what direction a thrown object was going to determine if it should collide with a wall or not. Each time we fixed the current issue, we’d find another scenario where the solution failed. This prompted me to do a quick search on the internet, which yielded little results. I’d been playing through The Legend of Zelda: Oracle of Seasons recently, so I decided to throw hundreds of bushes to figure out if a simpler solution could be accomplished. Eventually a light bulb went off and I realized how simple of a solution this really was. So, how do we throw objects in 2D top down videogames, much like the Zelda games of yesteryear?
How the 2D Zelda Games Probably Handled Throwing Objects
Since we’ve been developing Violet in Game Maker Studio 2, the code and examples will come from this engine. However, the concepts should be transferable to the engine of your choice.
We have two variables we keep track of for picking / lifting / holding / throwing. We are calling these variables holdingState
and holding
. holdingState
can have these values:
false
– we aren’t doing anything with holding objects"picking"
– we are currently in the picking up animation of an object"holding"
– we are currently holding the object over our head"throwing"
– we are currently in the throwing animation of an object
holding
represents a pointer to the object we are holding
. For example, if we are interacting with a rock, holding
would contain a reference
to be equal to that said rock (hero.holding.reference
gets us said rock). We’ll talk about holding
soon.
With these core variables in mind, we need the hero to start interacting and picking up the said objects. This could look like a million different things, but our check is simply check a spot in front of the hero they are facing and see if the spot contains an pickable
object. If it does, and the player is pressing input for picking up objects, then we begin our pickup script. We’ll talk about what picking up a pickable
object looks like soon. But let’s start to define what a holding
object looks like.
The holding
object is an invisible object that acts as a placeholder for the actual object being thrown. This is the solution to the aforementioned problem in the original illustration. See, by having an object act in the place of the object being thrown, we can fake a lot. The holding
object is always located at the hero’s x
and y
location, and we render the holding object above the hero’s head (or wherever).
We therefore have an illusion that the object is above the hero’s head, but in reality, it is located in the hero. When we throw the object, it simply acts like a normal projectile being shot from the hero, and therefore, won’t immediately collide with the wall when thrown.
The holding
object has a number of properties, and we’ll highlight a few now:
reference
– a pointer to the object being thrownholder
– a pointer to who is holding the object. If we set it tonoone
(null
in other languages), that means the object is being thrown.renderX
andrenderY
– the location of where it is being drawn — the illusionold_reference_mask_index
– we use the hero’s mask for theholdable
‘s collision so we ensure there is no way the object being thrown will hit the wall. This keeps track of the originalmask_index
of the object to be thrown.
Now that we understand the basics of the holding
object, let’s get back to picking up a pickable
object:
/// @description pickup_pickable(conditionIn, inst) picks up a pickable item /// @param conditionIn /// @param inst //assumes holding variable var conditionIn = argument0; var inst = argument1; var success = false; if (conditionIn) { holdingState = "picking"; holding = instance_create(inst.x, inst.y, obj_Holdable); //update some pointers holding.reference = inst; holding.holder = self; //we use the character mask here because the x and y location of holding is exactly the character //since we don't want the rock to collide with the wall when thrown, we want to make sure it uses the same mask holding.mask_index = spr_link_mask; //copy old mask index holding.old_reference_mask_index = inst.mask_index; //make the reference not have collisions inst.mask_index = spr_mask_none; //make the reference invisible inst.visible = false; success = true; } return success;
The biggest things to take away from this script are:
- We update
holdingState
to"picking"
and create theholding
pointer - We set the
holding
‘sreference
pointer to thepickable
object, and theholder
object to the hero (self
, orthis
in other languages) - We set the
holding
‘smask_index
to the hero so we ensure no collisions with the wall when thrown - We keep a copy of the
pickable
‘s original mask, and give it no mask so while it is moving around inside the hero, it won’t receive collisions - We hide the original
pickable
object, so we don’t see it inside the hero
Holdable
We can now talk in detail about the holding
object. If the hero is currently holding the holdable
object, we want to set the render
variables to a location where the hero is holding the object. If we are currently in a picking up animation, we do some math to tween the object from its starting place on the ground into the final position above the hero. We will always set its actual location (x
and y
values) to the hero’s (holder
‘s) location. Again, the rock is invisible, so we won’t actually see it in the hero.
We finally need to throw the object. Throwing is actually pretty simple, other than the “maths” to calculate where it should go. This again can be whatever feels right for your game, but we’re simply using some quadratic functions to tween the positions from the start of the throw to the where it should land.
When the player inputs the throw button, we should invoke the throw event of the holdable
object, and set the hero’s holdingState
to "throwing"
and set the holding
pointer to noone
.
The throw event for the holdable
object currently looks like this:
speed = 6; direction = holder.animation_direction; //the actual object (not the render, moves constantly) var dist = predict_distance(throwTime, speed, speed, ease_linear); //set final locations for actual and render throwYEnd = y + lengthdir_y(dist, direction); //set the starting location for the throw throwYStart = renderY; holder = noone; //the holder is deference since we are no longer holding
High level, this script sets the speed and the direction of the holdable
object to be thrown (animation_direction
is what direction the hero is facing, limited to 0
, 90
, 180
, 270
). Since we’re dealing with illusion values, we need to calculate where the ending location will be. We take the real location values and simply calculate linearly where the object will be in so many frames of game play (throwTime
).
Then, in our step event, our renderX
value will always be the real x
position of the holdable
object, but we can interpolate the y
value to give it the illusion that it is being thrown. Here’s an example of that:
if (direction_between(direction, 10, 170)) { renderY = ease_out_quad(thrownFramesCount, throwYStart, throwYEnd, throwTime); } else { renderY = ease_in_quad(thrownFramesCount, throwYStart, throwYEnd, throwTime); }
We should also point out that since the reference
‘s visibility is set to false, we can’t see it. In our drawing event of the holdable
object, we draw two things — the shadow of the reference
as well as the reference
.
if (holder == noone) { draw_shadow(x, renderY + shadow_yoffset + LINK_SHADOW_OFFSET); } draw_sprite_ext(reference.sprite_index, reference.image_index, renderX, renderY, reference.image_xscale, reference.image_yscale, reference.image_angle, reference.image_blend, reference.image_alpha);
When throwing is complete — whether the timer expired, or the holdable
object hits a wall (legitimately), we need to “turn the reference back on”. Each pickable
object has a break
event that we invoke at that time. Currently with our rock, we simply call a shatter
animation, but this could be whatever. We also want to turn visible
back on for the reference
object as well as reset the original mask from the holdable
‘s old_reference_mask_index
.
Conclusion
Though this post covered a lot, there were still a lot of of details not covered in this blog post. Therefore, we decided to host a simple example on Github. This example is a very simple, stripped down version of what Violet is currently using. You’re welcome to modify freely and use for your own projects.