Creating a Melee Weapon for Our FPS (FPS Series Part 10)

melee weapon in FPS game

This post is part of Godot FPS tutorial series.

Concept: In this tutorial, we’ll make a melee weapon using the same weapon system.

We have already made weapon_data.gd that holds melee specific information. In this tutorial, we will make use of it by implementing a knife.

Knife Equipped Scene Setup

Create a folder for Knife, and create a scene named KnifeEquipped.tscn with a Node3D root.

The only distinct thing in knife is that, instead fo hitscan (raycast), it has an Area3D node attached. When this Area3D node overlaps with some other body (such as enemies), it triggers same damage functions that hitscan weapon triggers when its raycast hits other bodies.

This si how KnifeEquipped.tscn looks like (notice the Area3D highlighted).

Knife Pickup Scene

Duplicate RevolverPickup.tscn, and change its appearance so it looks like a knife (copy MeshInstance3D nodes from knife equipped scene to the duplicated pickup scene). Also slightly change the collision shape’s size to match the object shape.

Creating Knife.tres (WeaponData)

Create a WeaponData resource for knife, and name it knife.tres. Put all the relevant information in it. Melee weapons ignore most of the attributes so just leave them as is. But make sure to assign KnifeEquipped.tscn & KnifePickup.tscn. Overall, my setup looks like this:

Scripting Our Knife

Attach a script to KnifeEquipped.tscn root node, and add following code.

# knife.gd
extends WeaponEquipped

@export_group("Stats")
@export var hit_area: Area3D

var can_damage: bool = false
@onready var swing_timer: Timer = $SwingTimer
var slash_tween: Tween
var original_rotation: Vector3

func _ready() -> void:
	super._ready()
	
	if hit_area:
		hit_area.body_entered.connect(_on_hit_area_body_entered)
	if weapon_model:
		original_rotation = weapon_model.rotation_degrees

# This is our knife "swing".
func attack() -> void:
	if not weapon_data:
		return
		
	# We can only swing if the timer isn't running and animation isn't playing.
	if not swing_timer.is_stopped() or (slash_tween and slash_tween.is_running()):
		return

	print("Knife: SWOOSH!")
	
	# --- Start Gameplay Logic ---
	can_damage = true
	hit_area.monitoring = true
	swing_timer.start()

	# --- Perform Slash Animation ---
	if weapon_model:
		if slash_tween:
			slash_tween.kill()

		slash_tween = create_tween()
		slash_tween.set_parallel(true)
		slash_tween.set_trans(Tween.TRANS_CUBIC)
		slash_tween.set_ease(Tween.EASE_OUT)
		
		# The SLASH
		slash_tween.tween_property(weapon_model, "position:z", weapon_model.position.z + weapon_data.slash_lunge_distance, weapon_data.slash_duration)
		slash_tween.tween_property(weapon_model, "rotation_degrees", original_rotation + weapon_data.slash_rotation_degrees, weapon_data.slash_duration)
		
		# The RETURN
		slash_tween.set_parallel(false)
		var return_tween = slash_tween.chain()
		return_tween.set_trans(Tween.TRANS_SINE)
		return_tween.set_ease(Tween.EASE_OUT)
		
		return_tween.tween_property(weapon_model, "position:z", _original_model_position.z, weapon_data.return_duration)
		return_tween.parallel().tween_property(weapon_model, "rotation_degrees", original_rotation, weapon_data.return_duration)


# Called when the swing duration is over.
func _on_swing_timer_timeout() -> void:
	can_damage = false
	hit_area.monitoring = false

# Called when something enters the knife's damage area.
func _on_hit_area_body_entered(body: Node3D) -> void:
	if not weapon_data:
		return
		
	if can_damage and body != shooter_body:  # Don't damage the shooter
		print("Knife hit: ", body.name)
		can_damage = false # Prevent multi-hits
		
		# Apply damage directly if the body can take damage
		if body.has_method("take_damage"):
			body.take_damage(weapon_data.damage)
			print("Dealt %d damage to: %s" % [weapon_data.damage, body.name])
		else:
			print("Hit object that can't take damage: %s" % body.name)
			
		emit_signal("deal_damage", weapon_data.damage, body.global_position, -global_transform.basis.z, body)

The above code connects the Area3D‘s body_entered signal to a function and does the hit logic there. The script also players some knife-y animations using Tweens.

Make sure to assign the Hit Area and other properties in the inspector of your KnifeEquipped.tscn scene:

Ignore those properties that aren’t present in your inspector. The above screenshot is from the final polished version where I added many minor tweaks not found in the tutorial.

Testing Your Knife

Now put your knife.tres in Weapon Data Array of WeaponManager node in Player.tscn. Thats all.

Leave a Reply

Your email address will not be published. Required fields are marked *