Making a Game - Programming the Weapons System - EP 16


Video Tutorial

Aims of this Tutorial

  • Create functions for managing laser length and battery glyph.
  • Create a weapons plan that will be shared between clients via the server.
  • Use this weapons plan in order to make a laser appear to fire.

Creating Helper Functions

Managing Laser Length

In order to manage the laser length, we need the following node structure for the gun model:

Spatial (Gun Container)
    Model (Gun model created last tutorial)
    RayCast (Will determine whether the laser hits)
        CSGCylinder (Visual representation of laser)

To control the laser, we will modify the height property of the CSGCylinder. This will also have the unintended side effect of extending in both directions. This will cause the cylinder to pass through our own ship! In order to remedy this situation, we will translate the cylinder out by half the height. The code for this is as follows:

func _update_laser(length : float):
	$GunContainer/RayCast/CSGCylinder.height = length
	$GunContainer/RayCast/CSGCylinder.translation = Vector3(0, length/2, 0)

Managing Battery Glyph

Managing the battery glyph will be significantly easier, since all we need to do is have a blocking ColorRect of the colour of the background in order to obscure the battery filling. We can then programmatically control the margins with a linear relationship to show how much energy is left in the batteries. My code is shown below, but you may need to adjust the numbers for your specific icons:

func _update_battery_visuals(value : float):
	# Assume value is between 0 and 100
	$WeaponsPanel/FillingBlocker.margin_bottom = 65 - (value * 1.2)

Creating a Weapons Plan

Since we already did all the hard work, a lot of this will be using the infrastructure we left behind for the thrust plans. The first thing to do will be to create a weapons plan element which will be derived from a plan element. This will have a sanity check that simply returns true and has a boolean for whether the gun is firing or not. This simple class is displayed below:

extends "res://Scripts/"
class_name WeaponsElement

var firing : bool = false

func sanity_check() -> bool:
	return true

Now we can create a weapons plan property of type Plan in our player script. We need to populate it in the same way we did with the thrust plan. We can call the same remove_last_plan(), clear_plan(), etc for all the buttons except add and end turn. These require special attention as there is some nuance here. For the add button, we need to create a weapon plan element and use the same time, but then get our firing status from a check button in the ui. We can then add it into the weapons plan. The addition to the add button callback is:

var new_weapons_element : WeaponsElement =
new_weapons_element.firing = $WeaponsPanel/WeaponsSwitch.pressed
new_weapons_element.time = $MainPanel/TimeBar.value - weapons_plan.current_time()
if (weapons_plan.add_element(new_weapons_element)):
	get_parent().get_parent().console_print(String(is_local) + ": Added weapons element with " + String(new_weapons_element.firing))
	get_parent().get_parent().console_print(String(is_local) + ": Failed to add weapons element")

Ok, so now we have a local weapons plan, but we need to share it, so we need to use the end turn button to do this! We can simply call a new function, send_weapons_plan(), before sending the end_turn rpc call to the server and then ending the turn on the weapons plan afterwards. This function will be similar to send_thrust_plan().

func send_weapons_plan() -> void:
	for element in weapons_plan.current_elements:
		get_parent().get_parent().console_print('Sending data = ' + String(element.time) + String(element.firing))
		rpc_id(1, "add_weapons_element", element.time, element.firing)

As you can see, this calls add_weapons_element(time, firing)on the server for each element. Again, we are just following the motions that we did for the thrust plan. On the server we have:

remote func add_weapons_element(time: float, firing: bool) -> void:
	var element : WeaponsElement =
	element.time = time
	element.firing = firing
	get_parent().get_parent().console_print("Adding weapons element with the following params: time = " + String(time) + ", firing = "+ String(firing))

Now the server has everyone's weapons plans, so we just need to redistribute them. This is done after everyone has ended their turn, when send_all_plans() is called on each player on the server. We need to send off the plans there and end the turns locally. The function should now look like this:

func send_all_plans() -> void:
	get_parent().get_parent().console_print("Sending plan...")
	for element in thrust_plan.current_elements:
		rpc("add_thrust_element", element.time, element.linear_thrust, element.rotational_thrust)
	for element in weapons_plan.current_elements:
		rpc("add_weapons_element", element.time, element.firing)

We see the rpc call add_weapons_element(time, firing) and we note that we need that remote function on the clients. Again, this closely resembles the thrust plan version:

remote func add_weapons_element(time : float, firing : bool) -> void:
	if is_local:
	var element : WeaponsElement =
	element.time = time
	element.firing = firing
	get_parent().get_parent().console_print(String(is_local) + ": Adding weapons element with the following params: time = " + String(time) + ", firing = " + String(firing))

We also need to modify r_end_turn(), the remote function on the clients that is called at the end of the turn by ending the weapons plan at the end of that function. Now everyone has the weapons plan, but no-one knows what to do with it...

Making the Weapons Plan do Something

For now, we will simply make the laser visually fire. We therefore need to modify the physics process loop. We will employ 2 helper functions to achieve this. One will be to get all the weapon plan elements (like we did for the thrust plans) and another will fire the laser. The one that gets all the weapon plan elements will be:

func _get_all_weapons_elements():
	var all_elements = []
	for element in weapons_plan.elements:
	for element in weapons_plan.current_elements:
	return all_elements

The one that fires the laser is a little more complicated because there are 2 scenarios: either we hit an object and laser goes to that object, or we don't and the laser goes to it's max length (500 in my case). Missing is the easy case because we have a fixed length. But if we miss, we need to calculate the distance between the raycast and the target. We can only get the target position in the global coordinate system, so we need to get the raycast global position which we get from it's global transform's origin. The function is shown below:

func _fire_gun():
	$GunContainer.visible = true
	if $GunContainer/RayCast.is_colliding():
		var raycast_pos : Vector3 = $GunContainer/RayCast.global_transform.origin
		var target_pos : Vector3 = $GunContainer/RayCast.get_collision_point()
		var diff : Vector3 = raycast_pos - target_pos

Now we can use both functions in order to fire the gun when we are supposed to be firing our gun in the physics process function:

var all_weapons_elements = _get_all_weapons_elements()
var current_weapons_element : WeaponsElement = _get_current_plan_element(all_weapons_elements)
if current_weapons_element == null:
	$GunContainer.visible = false
	running_plan = false
if current_weapons_element.firing:
	$GunContainer.visible = false

Now test your game and make sure that you can fire the weapons and transfer the plan to the other player. That is all for this tutorial, next time we will be dealing some damage!