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
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
disable_player() respectively. What they do is to turn on or to turn off following aspects of the player controller:
- The value of the
- 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:
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_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
drop_vehicle functions are called without overriding them. That way I introduced two callbacks
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.
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.
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.