-
Notifications
You must be signed in to change notification settings - Fork 0
Raycasting, Projectiles, and Hitboxes
Detecting when an object is hit by another in game development is essential to almost every genre--- think platformer games with side-scrolling enemies, stealth games about sneaking around guards, space-shooter games where spaceships launch projectiles, etc. Many of these concepts are connected in the sense that you're trying to detect when objects in your game are positioned in a specific way relative to one-another. There are a few technologies that are most commonly used to accomplish some of these interactions. We'll go over them all briefly, and their use cases in this tutorial, alongside some brief implementations and the like, but as per usual the documentation on the official Godot wiki is the best resource available.
Hitboxes are a basic principle in game development that describe the physical properties of an object in a game. Whereas a mesh (created in external CAD software, such as Blender) defines an object's visual appearance through the usage of polygons, hitboxes define the way objects collide with their environment, interact with raycasts (more on this later), and even how they affect other objects with hitboxes.
It's really slow and resource-inefficient to calculate collisions on detailed models. One can simplify most models to one (or a few more, depending on the use case) simpler shapes (usually boxes or cylinders) and save hugely on performance. For this reason, most game engines will require you to provide a collider.
In Godot, we have the CollisionShape2D/3D Node, which is applied to RigidBody nodes to create physics-affected objects and CharacterBodies.
We can also use the Area nodes to define hitboxes which don't engage in the physics simulation but can still detect for other Area nodes or collision bodies.
Raycasting is a method of drawing a straight line to check for collisions with solid bodies, first developed to aid in the rendering of 3D Graphics. Raycasts in Godot (also referred to as hitscans) check for CollisionBodies which lay upon a line in 2D space. They occur instantaneously, are unaffected by physics, and (by default) have no visual or physical appearance--- they are just an algorithmic method of finding intersections on a line with code. They are often used to simulate ballistics or lasers in shooter games, and the same algorithms are used for even modern rendering systems like ray-traced lighting.
Ray casting in Godot can seem daunting at first, but the documentation walks through it in great detail: Link
To summarize, you must first access the physics engine of Godot (the "space") in a script by using the get_world_2d/3d().space
method. This will return a Resource ID (RID) which is just an identifier for Godot's lower-level resources--- don't worry, since Godot is loosely typed, you can just use the var
keyword to define variables containing the "space". You'll have to access the "space" in the _physics_process()
method callback due to some runtime restrictions.
After accessing the current space state, you're then able to perform a raycast query and the results returned will contain the position of the collision and other vital information.
Note about raycast results: raycasts will return an empty dictionary, represented in GDScript with the syntax {}
if the raycast doesn't intersect a collider (essentially, if it misses). Check using an if-statement that the raycast result is not empty to avoid errors.
As mentioned previously, raycasts are often a good way to simulate ballistics or lasers between points (i.e. the cannons on a space-ship to the mouse's position). Something like this would do the trick in 2D.
func _physics_process(delta):
var space_state = get_world_2d().direct_space_state
# use global coordinates, not local to node
var query = PhysicsRayQueryParameters2D.create(position, get_viewport().get_mouse_position()) # This will fire a raycast between the position of the Node this script is attached to and the position of the mouse, in 2D space.
var result = space_state.intersect_ray(query)
The only problem here is that you might not want a raycast to collide with certain objects (i.e. obstacles that have colliders but shouldn't interact with the player's lasers). The PhysicsRayQueryParameters class can take a collision_mask argument when it is created, which expects a bitmap. If this issue arises, you should see the Collision layers and masks documentation from Godot, as this is a far more indepth subject.
If you'd like to render the trajectory of a raycast, consider using the Line2D Node, or the draw_line()
method.
Other common uses of raycasting include line-of-sight (ensuring one object can "see" another, without obstruction), and pathfinding.
Some games use physics-simulated projectiles for their ballistics simulations. This is typically more intuitive, as the projectile would behave the exact same as any other RigidBody. It is usually accomplished by applying an impulse force to the projectile and allowing it to fly until it collides with a CollisionBody (use the body_entered signal).
A simpler approach is to use an Area2D with a collision shape and use a script to repeatedly move it in the direction it is fired. This can be less resource intensive and more consistent if you don't want to deal with Rigidbody physics. Take this example (which would be applied to a projectile scene / prefab):
var speed = 100
func _ready():
look_at(target)
func _physics_process(delta):
position.x += transform.x * speed * delta
The only issue here then becomes ensuring the positive X direction is the direction the projectile's sprite faces. Additionally, some problems can occur from fast-travelling projectiles that are being purely physics-driven. A solution to this is to repeatedly draw tiny raycasts in the direction of travel, but this is an advanced solution for an issue that only really arises in neiche cases.
Pros:
- Less performance heavy
- Supports many usecases (vision, pathfinding)
- Can feel "crisper" --- instantaneous feedback provides instant satisfaction
Cons:
- Can feel too easy, and require artificial random inaccuracy to feel balanced
- Needs rendering
Pros:
- Feels more skillful
- Can be more "arcade-y"
- More control over balance and hitbox size
Cons:
- Performance heavy
- Issues at high speeds
Both raycasting and physics-projectiles have their pros and cons, and they should thus be used in different circumstances. Games like Quake (and by extension, the many games derived off of it) famously use both hitscan and projectile-based ballistics systems, simulating grenades and rockets with physics and bullets with raycasts. This actually provides an interesting challenge, as slow-moving rocket projectiles require predictive action to hit other players, while paying off a higher damage reward.
In some cases, games may also opt to use projectiles for attacks from AI opponents while keeping player's attacks hitscan. Generally speaking, unless you want your game to be intentionally punishing, hitscan ranged attacks for enemies can feel unfair, not allotting the player the chance to use their skill to dodge the attack. The crisp feedback the player gets from hitting a hitscan attack cannot be said about receiving one.