Vehicle mode for FPS games in Godot

From all FPS games, my favorite titles are those with vehicle mode, like Battlefield series. Therefore, when I started to work on my own FPS prototype with Godot Engine, I decided to introduce something similar. Surely, I do not pretend to create a complete simulation of battle vehicles; only essential functionality with primitive arcade-like physics. That is how I ended with the following solution. I am still on very early stages in game development, so I admit that my idea is not perfect and maybe I brought concepts from web development, that should be avoided in game programming. However, it is a working concept, and due to the fact, that existing Godot tutorials on vehicle mode are not complete, let me present you my own.

Player vs. vehicle mode

The first thing first is to distinguish between two main game modes: player mode (this is a first-person mode, where you control the character) and vehicle mode (this is a third-person view, where vehicle is controlled). What I found out during my research, that some Godot tutorials treat both modes as a single and just switch cameras views. I don’t think it is correct at all, as the whole idea of a vehicle mode in the game like the mentioned Battlefield is to be able to pick up a vehicle. Therefore, if you “carry” vehicle always with player you can’t do embark/disembark and also it will be complicated to introduce more than one vehicle. I even don’t mention different type of vehicles that can be available in your game – ground vehicles, air vehicles, ships (ships?) and so on.

In my prototype I have a quite naive first person controller, that is placed inside the Player.gd script. This component can move around, rotate camera, fire etc – in other words it is responsible for the player mode. There is a state variable is_active, which defines, if the mode is active. If it is not active, the controller will not work. Take a look on the _physics_process() implementation:

func _physics_process(delta):

    if is_active:

        var direction = Vector3()
        var head_basis = head.global_transform.basis

        if Input.is_action_pressed("move_forward"):
            direction -= head_basis.z
        elif Input.is_action_pressed("move_backward"):
            direction += head_basis.z

        if Input.is_action_pressed("move_left"):
            direction -= head_basis.x
        elif Input.is_action_pressed("move_right"):
            direction += head_basis.x

        direction = direction.normalized()

        # gravity
        apply_gravity()

        # switch weapons
        switch_weapons()

        if Input.is_action_just_pressed("primary_fire"):
            if active_weapon.is_possible_shoot:
                active_weapon.shoot()

        if Input.is_action_pressed("reload"):
            active_weapon.reload()

        # switch weapons with two buttons (to test gamepad without gamepad):
        if Input.is_action_just_pressed("weapon_switch_up"):
            weapon_switch_up()
        elif Input.is_action_just_pressed("weapon_switch_down"):
            weapon_switch_down()

        # jump
        jump()

        var speed = run_speed if Input.is_action_pressed("move_run") else normal_speed

        velocity = velocity.linear_interpolate(direction * speed, acceleration * delta)

        velocity = move_and_slide(velocity, Vector3.UP)

        update_ui()

When the value of the variable is_active_ is false, this code snippet will not be executed. I introduced two other helper methods, to trigger the player mode, e.g. to enable it, when the player disembarks a vehicle and to disable it, when the player is inside a vehicle. These two methods are called enable_player() and disable_player() respectively. What they do is to turn on or to turn off following aspects of the player controller:

  • The value of the is_active variable
  • Visibility of the player controller
  • Enabling/disenabling of the collision body
  • First person camera
  • Interaction raycast (we will talk about it later on)

My sample implementations of these methods are presented below:

func enable_player():
    is_active = true
    visible = true
    camera.current = true
    interaction_raycast.enable()
    collision_shape.disabled = false


func disable_player():
    is_active = false
    visible = false
    camera.current = false
    interaction_raycast.disable()
    collision_shape.disabled = true

On the other hand I have the Vehicle.gd class which serves as a parent class for all vehicle types. It is responsible for the vehicle mode. There are two helper methods, that are used to trigger the mode: use_vehicle() and drop_vehicle(). The vehicle entity also has it is own variable to keep an active stay – is_activated. Also these methods are used to switch camera and to notify the player controller to enable or disable the player mode.

Take a look on the sample implementation below:

func use_vehicle():
    print('Use vehicle')
    is_activated = true
    camera.current = true
    do_on_embark()
    get_tree().call_group('Player', 'disable_player')
    get_tree().call_group('UI', 'activate_vehicle_ui', true)


func drop_vehicle():
    print('Drop vehicle')
    is_activated = false
    camera.current = false
    do_on_disembark()
    get_tree().call_group('Player', 'enable_player')
    get_tree().call_group('UI', 'activate_vehicle_ui', false)

You may notice here two callback methods do_on_embark() and do_on_disembark(). We will return to them later. Before, I would like to talk about how to interact with a vehicle.

How to embark and to disembark

In the previous subsection, I mentioned an interaction raycast. This component is used by the player controller to interact with various game objects, such as doors, locks, light switches etc. It is decomposed from the main Player.gd script and is placed into the Interaction.gd class. By default, the idea is following: when a player interacts with an interactable object, a hint is shown and a player can use that object. For vehicle it means to embark.

So, we can embark the car by pressing the E button. In this situation we call the use_vehicle method to activate the vehicle mode. This works as following:

func _process(delta):
    var collider = get_collider()

    if is_colliding():
        if current_collider != collider:
            current_collider = collider
            if collider is Vehicle:
                var text = 'use vehicle'
                set_interaction_text(text)

        if Input.is_action_just_pressed('interact'):
            if collider is Vehicle:
                var vehicle = collider as Vehicle
                vehicle.use_vehicle()

Please note, that this is short form of the original code, because it also deals with Interactable objects, that are out of the scope of this post; so I limited it to interactions with Vehicle only. With all this logic we now can interact with vehicles and switch between two game modes.

I have mentioned already what I call callback functions. Being mainly web developer, I took that idea from there, so I admit it may be not good for video games. Because we have a parent class Vehicle that encapsulates core embark/disembark functionality, we may need to do something when use_vehicle and drop_vehicle functions are called without overriding them. That way I introduced two callbacks do_on_embark and do_on_disembark, where concrete implementations can put own logic without overriding of parent implementations. There is another callback do_movement, but this is a separate story, that we will discover in the next subsection.

How vehicles do move

Same way as with the first person controller, we have the variable that keeps if the current state is active. For vehicles it is called is_activated and is placed inside the parent class. It is also utilized in the _physics_process() function that listens game updates. Once the vehicle mode is activated, the function will call the do_movement callback, which is used to move the vehicle. As there are different types, it can’t be implemented in the core class.

func _physics_process(delta):
    if is_activated:
        if Input.is_action_just_pressed("interact"):
            drop_vehicle()
        do_movement(delta)

My sandbox project, that you can find on github, contains a sample vehicle implementation – a police car. It is not a perfect simulation of a car behavior (if at all), and I took concrete logic from this wonderful kinematic car tutorial from KidsCanCode. You can find explanations there, because I could not copy-paste it and claim that the solution is mine, of course, it is not. What I want to show you instead, is how it is organized.

All moving logic of the police car is placed inside the overridden do_movement callback. Due to the fact, that the parent Vehicle class passes the delta value to the callback, we can use it to perform movements of vehicles. For my police car, I did it as following:

func do_movement(delta):
    if is_on_floor():
        handle_input()
        handle_friction(delta)
        handle_steering(delta)
    car_acceleration.y = gravity
    velocity += car_acceleration * delta
    velocity = move_and_slide_with_snap(velocity, -transform.basis.y, Vector3.UP, true)

The whole movement implementation is taken from the KidsCanCode, so I would not stop on it. What is crucial here, is that we can use do_movement callback to implement moving of any type of vehicles; not limited to cars, but also helicopters, air crafts, space ships etc.

Source code

This post is based on my FPS sandbox project. It is available in this github repository and has much more, than just the vehicle mode. Feel free to fork it and experiment with it. If you have questions or suggestions or any valuable feedback, don’t hesitate to contact me or drop a comment.

Conclusion

That is the way I used to implement the vehicle mode for FPS game. I admit that is not the best one, and I excited to listen your solutions, admitting the fact, that I am quite new to Godot Engine and game dev in general. However, this code does its job and provides the complete experience – both interaction and movements and it separates two modes, and provides a possibility to extend the original functionality for any type of vehicles.