Skip to content

Commit 8bbacb7

Browse files
authored
Merge pull request #9 from GDquest/features/project-asset
Project Asset
2 parents 6a29f5c + 97e0466 commit 8bbacb7

File tree

17 files changed

+160
-57
lines changed

17 files changed

+160
-57
lines changed

Game.tscn

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
[gd_scene load_steps=11 format=2]
1+
[gd_scene load_steps=10 format=2]
22

3-
[ext_resource path="res://src/Camera/Camera.tscn" type="PackedScene" id=1]
4-
[ext_resource path="res://src/Player/Player.tscn" type="PackedScene" id=2]
3+
[ext_resource path="res://src/Player/Player.tscn" type="PackedScene" id=1]
54

65
[sub_resource type="SpatialMaterial" id=1]
76
albedo_color = Color( 1, 0.537255, 0.8, 1 )
@@ -37,6 +36,7 @@ mesh = SubResource( 2 )
3736
material/0 = null
3837

3938
[node name="StaticBody" type="StaticBody" parent="TestMap/Ground"]
39+
editor/display_folded = true
4040

4141
[node name="CollisionShape" type="CollisionShape" parent="TestMap/Ground/StaticBody"]
4242
shape = SubResource( 3 )
@@ -169,7 +169,4 @@ shape = SubResource( 8 )
169169
transform = Transform( 0.766044, 0.166366, -0.620885, 0.271654, 0.791635, 0.547283, 0.582563, -0.587909, 0.561234, 0, 12, 0 )
170170
shadow_enabled = true
171171

172-
[node name="Camera" parent="." instance=ExtResource( 1 )]
173-
transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.75, 0 )
174-
175-
[node name="Player" parent="." instance=ExtResource( 2 )]
172+
[node name="Player" parent="." instance=ExtResource( 1 )]

README.md

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,72 @@
1-
# Godot 3d Mannequin
1+
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
2+
**Table of Contents**
3+
4+
- [Quick Start Guide](#quick-start-guide)
5+
- [Controls](#controls)
6+
- [How it works](#how-it-works)
7+
- [Player](#player)
8+
- [Camera](#camera)
9+
- [Configuration](#configuration)
10+
- [Customization](#customization)
11+
- [Credits](#credits)
12+
- [Support our work](#support-our-work)
13+
14+
<!-- markdown-toc end -->
215

316
An Open Source 3d character and character controller for the Godot game engine.
417

518
![The mannequin in-game](./assets/screenshots/prototype-1.png)
619

720
This is a third person character controller designed to work both with the keyboard and a gamepad. It features a camera that can auto-rotate or that can be controlled with a joystick.
821

9-
## Credits
22+
# Quick Start Guide #
23+
24+
The 3D Third Person Character Controller is made of two scenes:
25+
26+
* `Camera.tscn` - A 3D camera rig with a state machine for aiming
27+
* `Player.tscn` - A `KinematicBody` with a state machine for player movement. Contains an instance of `Camera`. It also includes the animated 3D mannequin.
28+
29+
To use the default character, instance `Player` in your game. See `Game.tscn` for an example. In this demo, the obstacles are mesh instances with static body collisions making up a cube world.
30+
31+
## Controls ##
32+
33+
The game supports both mouse and keyboard, and the gamepad.
34+
35+
# How it works #
36+
37+
## Player ##
38+
39+
The scene that deals with the movement, collision, and logic of the player. The player is a KinematicBody with a capsule collision shape, and the movement logic is within a [Finite State Machine](http://gameprogrammingpatterns.com/state.html).
40+
41+
The scene also holds an instance of the `PlayerMesh` for animation purposes. This scene lives in the `PlayerMesh.tscn` scene. It holds the skeletal rig for the mesh's animation, the 3D model of the body and head sepearately, and the animation tree and player to control the animation workflow of the model. The lot is wrapped up in a spatial node with some logic to transition to which animation based on which state the player is in.
42+
43+
## Camera ##
44+
45+
The scene that deals with the Camera movement. It follows the Player in the game, but in code it moves and rotates separately from it. It has a `SpringArm` node to help with preventing collision with level geometry - moving the viewpoint forwards to prevent moving the camera inside geometry. It also has a system that holds the raycast for aiming-mode, and the 3D sprite that is a projected reticule. The logic is held in a finite state machine.
46+
47+
# Configuration #
48+
49+
To change the player and the camera's behavior, you need to change properties on the corresponding states in their state machine.
50+
51+
Most of the configuration available for player movement are located on the `Move` state in the Player scene - the player speed and the rotational speed.
52+
53+
The Camera has more options. On the main Camera state in the Camera scene are items like the default field of view, whether Y is inverted, and sensitivity.
54+
55+
In addition, the Aim state allows some finer-tuned changes, like whether the aiming camera is first or third person, and by how much it should be offset over-the-shoulder of the character.
56+
57+
# Customization #
58+
59+
While the scenes can be modified extensively with new nodes and raw code, the state machine model allow for some simple, new functionality with relative ease.
60+
61+
As an example, there is the `Extensions` folder which contains additional player states for using the aiming view to fire a hookshot that pulls you towards the reticle. Once those states have been added to the Player's `Move` state, you only need to replace the return statement in Move's `enter` with code like `owner.camera.connect("aim_fired", self, "on_Camera_aim_fired")` and Move's `exit` with code like `owner.camera.disconnect("aim_fired", self, "on_Camera_aim_fired")`
62+
63+
# Credits #
1064

1165
1. The Godot mannequin is a character made by [Luciano Muñoz](https://twitter.com/lucianomunoz_) In blender 2.80.
1266
1. Godot code by Josh aka [Cheeseness](https://twitter.com/ValiantCheese)
67+
1. Additional code by Francois Belair aka [Razoric480](https://twitter.com/Razoric480)
1368

14-
## Support our work
69+
# Support our work #
1570

1671
This free series is sponsored by our [platformer game creation course](https://gdquest.mavenseed.com).
1772

icon.png

2.48 KB
Loading

src/Camera/Camera.gd

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1+
tool
12
extends Spatial
3+
"""
4+
Simple accessor class to let the nodes in the Camera scene access the player
5+
or some frequently used nodes in the scene itself caching the call to get_node
6+
"""
27

38

49
signal aim_fired(target_vector)
510

611
onready var camera_state: State = $StateMachine/Camera
712
onready var aim_ray: RayCast = get_node("AimRay")
813

9-
var player: Player
14+
var player: KinematicBody
1015

1116

1217
func _ready() -> void:
13-
player = get_tree().root.find_node("Player", true, false)
14-
assert(player)
18+
set_as_toplevel(true)
19+
yield(owner, "ready")
20+
player = owner
21+
22+
23+
func _get_configuration_warning() -> String:
24+
return "Missing player node" if not player else ""

src/Camera/Camera.tscn

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
[node name="Camera" type="Spatial"]
1111
script = ExtResource( 1 )
1212

13-
[node name="Camera" type="Camera" parent="."]
14-
transform = Transform( 1, 0, 0, 0, 0.866025, 0.5, 0, -0.5, 0.866025, 0, 3, 5 )
15-
current = true
13+
[node name="SpringArm" type="SpringArm" parent="."]
14+
transform = Transform( 1, 0, 0, 0, 0.87462, 0.48481, 0, -0.48481, 0.87462, 0, 0.5, 0.5 )
15+
spring_length = 5.0
16+
margin = 0.5
1617

17-
[node name="OcclusionRay" type="RayCast" parent="."]
18+
[node name="Camera" type="Camera" parent="SpringArm"]
19+
current = true
1820

1921
[node name="AimRay" type="RayCast" parent="."]
2022
transform = Transform( 1, 0, 0, 0, 0.866025, 0.5, 0, -0.5, 0.866025, 0, 0, 0 )

src/Camera/CameraStates/Aim.gd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
extends State
2+
"""
3+
Logic state for the camera where configuration are made to the camera to set
4+
the field of view, position offset (over the shoulder), etc.
5+
"""
26

37

48
export var first_person_aiming: bool = false

src/Camera/CameraStates/Camera.gd

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
extends State
2+
"""
3+
Parent state for all camera based states for the Camera. Handles input based on
4+
mouse/gamepad and movement and configuration based on which state we're using.
25
6+
This state holds all of the main logic, with the state itself being configured
7+
or have its functions overriden or called by the child states. This keeps the
8+
logic contained in a central location while being easily modifiable.
9+
"""
310

4-
onready var camera: Camera = owner.get_node("Camera")
11+
12+
onready var camera: SpringArm = owner.get_node("SpringArm")
13+
onready var camera_view: Camera = camera.get_node("Camera")
514
onready var initial_position: = camera.translation
615
onready var initial_anchor_position: Vector3 = owner.translation
716

8-
onready var occlusion_ray: RayCast = owner.get_node("OcclusionRay")
917
onready var aim_target: Sprite3D = owner.get_node("AimTarget")
1018

1119
export var default_fov: = 70.0
@@ -22,7 +30,6 @@ var _is_aiming: = false
2230

2331

2432
func _ready():
25-
occlusion_ray.set_cast_to(initial_position)
2633
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
2734

2835

@@ -58,18 +65,10 @@ func physics_process(delta: float) -> void:
5865
elif owner.rotation.y < -PI:
5966
owner.rotation.y += 2 * PI
6067

61-
if camera.fov != _current_fov:
62-
camera.fov = lerp(camera.fov, _current_fov, 0.05)
68+
if camera_view.fov != _current_fov:
69+
camera_view.fov = lerp(camera_view.fov, _current_fov, 0.05)
6370

64-
# If there is a body between the camera and the player, move the camera closer
65-
occlusion_ray.force_raycast_update()
66-
if occlusion_ray.is_colliding():
67-
var global_offset = camera.global_transform
68-
if global_offset.origin != occlusion_ray.get_collision_point() + _offset:
69-
global_offset.origin = occlusion_ray.get_collision_point() + _offset
70-
camera.global_transform = global_offset
71-
elif camera.translation != initial_position + _offset:
72-
camera.translation = lerp(camera.translation, initial_position + _offset, 0.05)
71+
camera.translation = lerp(camera.translation, initial_position + _offset, 0.05)
7372

7473

7574
func enter(msg: Dictionary = {}) ->void:
@@ -142,4 +141,4 @@ static func get_move_direction() -> Vector3:
142141
Input.get_action_strength("move_right") - Input.get_action_strength("move_left"),
143142
0,
144143
Input.get_action_strength("move_back") - Input.get_action_strength("move_front")
145-
)
144+
)

src/Camera/CameraStates/CameraDefault.gd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
extends State
2+
"""
3+
Logic state for the camera - implies no modification to the camera settings.
4+
"""
25

36

47
func unhandled_input(event: InputEvent) -> void:

src/Player/MoveStates/Hang.gd renamed to src/Extensions/PlayerStates/Hang.gd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
extends State
2+
"""
3+
State that does not involve gravity or further movement until released.
4+
"""
25

36

47
func unhandled_input(event: InputEvent) -> void:

src/Player/MoveStates/Zip.gd renamed to src/Extensions/PlayerStates/Zip.gd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
extends State
2+
"""
3+
Hookshot style state for the player - gets a destination from an outside
4+
piece of logic (in this case, the Camera's aim state firing with a raycast)
5+
and flies through the air to reach it.
6+
"""
27

38

49
var zip_speed:= 10.0

src/Player/MoveStates/Air.gd

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
extends State
2+
"""
3+
State for when the player is jumping and falling.
4+
"""
25

36

47
func physics_process(delta: float) -> void:
5-
#TODO: Adjust peak of jump height depending on when jump input is released
68
_parent.velocity -= Vector3(0, delta * _parent.max_speed.y, 0)
79
_parent.physics_process(delta)
810

911
if owner.is_on_floor():
1012
_state_machine.transition_to("Move/Idle")
1113
if owner.is_on_ceiling():
1214
_parent.velocity.y = 0
13-
#if at ledge, transition to ledge
1415

1516

1617
func enter(msg: Dictionary = {}) -> void:
@@ -19,4 +20,4 @@ func enter(msg: Dictionary = {}) -> void:
1920

2021

2122
func exit() -> void:
22-
_parent.exit()
23+
_parent.exit()

src/Player/MoveStates/Idle.gd

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
extends State
2+
"""
3+
State for when there is no movement input.
4+
Supports triggering jump after the player started to fall.
5+
"""
26

37

48
onready var jump_delay: Timer = $JumpDelay
@@ -29,4 +33,4 @@ func enter(msg: Dictionary = {}) -> void:
2933

3034

3135
func exit() -> void:
32-
_parent.exit()
36+
_parent.exit()

src/Player/MoveStates/Move.gd

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
extends State
2+
"""
3+
Parent state for all movement-related states for the Player.
4+
5+
Holds all of the base movement logic.
6+
Child states can override this state's functions or change its properties.
7+
This keeps the logic grouped in one location.
8+
"""
29

310

411
export var max_speed: = Vector3(50.0, 50.0, 500.0)
@@ -17,32 +24,36 @@ func unhandled_input(event: InputEvent) -> void:
1724
func physics_process(delta: float) -> void:
1825
var input_direction: = get_input_direction()
1926

20-
#The basis holds the (right, up, and -forwards) vectors of our camera.
21-
#Multiplied by our input and summed together gets us a final direction vector relative to the camera
27+
# Calculate a move direction vector relative to the camera
28+
# The basis stores the (right, up, -forwards) vectors of our camera.
2229
var forwards: Vector3 = owner.camera.global_transform.basis.z * input_direction.z
2330
var right: Vector3 = owner.camera.global_transform.basis.x * input_direction.x
2431
var move_direction: = (forwards + right).normalized()
2532
move_direction.y = 0
2633

27-
#Rotation
28-
owner.look_at(owner.global_transform.origin + move_direction, Vector3.UP)
34+
# Rotation
35+
if move_direction:
36+
owner.look_at(owner.global_transform.origin + move_direction, Vector3.UP)
2937

30-
#Movement
38+
# Movement
3139
var new_velocity = calculate_velocity(velocity, max_speed, move_speed, delta, move_direction)
3240
if new_velocity.y == 0:
3341
new_velocity.y = -0.01
3442
owner.move_and_slide(new_velocity, Vector3.UP)
3543

3644

3745
func enter(msg: Dictionary = {}) -> void:
38-
owner.camera.connect("aim_fired", self, "on_Camera_aim_fired")
46+
return
3947

4048

4149
func exit() -> void:
42-
owner.camera.disconnect("aim_fired", self, "on_Camera_aim_fired")
50+
return
4351

4452

45-
func on_Camera_aim_fired(target_vector: Vector3) -> void:
53+
"""Callback to transition to the optional Zip state
54+
It only works if the Zip state node exists.
55+
It is intended to work via signals"""
56+
func _on_Camera_aim_fired(target_vector: Vector3) -> void:
4657
_state_machine.transition_to("Move/Zip", { zip_target = target_vector })
4758

4859

src/Player/MoveStates/Run.gd

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
extends State
2+
"""
3+
Basic state when the player is moving around until jumping or lack of input.
4+
"""
25

36

47
func unhandled_input(event: InputEvent) -> void:
@@ -18,4 +21,4 @@ func enter(msg: = {}) -> void:
1821

1922

2023
func exit() -> void:
21-
_parent.exit()
24+
_parent.exit()

src/Player/Player.gd

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
tool
12
extends KinematicBody
23
class_name Player
4+
"""
5+
Helper class for the Player scene's scripts to be able to have access to the
6+
camera and its orientation.
7+
"""
38

49

5-
var camera: Camera
10+
onready var camera: Spatial = $Camera
611

712

8-
func _ready() -> void:
9-
camera = get_tree().root.find_node("Camera", true, false)
10-
assert(camera)
13+
func _get_configuration_warning() -> String:
14+
return "Missing camera node" if not camera else ""

0 commit comments

Comments
 (0)