I Can't Get RPG Dialogue to Appear Above 3D Character's Head

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Sdhy

I’m using a cube MeshInstance as the RPG-character.

So I have 4 nodes in a 3D scene:

  • Cube_KinematicBody
    – Cube_MeshInstance
  • Camera
  • Label

The kinematic body gets its own global 3D location (which is the same as the cube’s),

The camera gets this 3D location and turns it into a 2D location on the computer screen.

I set these variables under func _process(delta).
And these variables change and seem to represent the cube’s 3D or 2D-screen location as I move the character around in-game.

But when I try to use the screen-location in the Label node, its value stops changing as I move around.

I thought if I used get_global_transform() to get the cube’s location,
it’d be global.

How can I get RPG dialogue to appear above a 3D character’s head in a 3D game?

Here’s my code:
-------KINEMATIC NODE------

extends KinematicBody

var direction = Vector3(0,0,0)
var myCube3DLoc = Vector3(0,0,0)


func _process(delta):		#Use this for things you want to work every frame. e.g. movement.
	myCube3DLoc.x = get_global_transform().origin.x
	myCube3DLoc.y = get_global_transform().origin.y
	myCube3DLoc.z = get_global_transform().origin.z
#	print("myCube3DLoc using get_global_transform().origin.x, y, or z returns: ", myCube3DLoc)

-------CAMERA NODE-------

extends Camera

onready var myCube3DLoc = get_node("../Cube_KinematicBody").myCube3DLoc
onready var myCubeScrnLoc = unproject_position(myCube3DLoc)
	#unproject_position(Vector3) returns the 2D coordinate in the Viewport rectangle that maps to the given 3D point in worldspace.


func _process(delta):
	myCube3DLoc = get_node("../Cube_KinematicBody").myCube3DLoc
	print("Camera node says... unproject_position(myCube3DLoc) returns: ", unproject_position(myCube3DLoc))

-------LABEL NODE-------

extends Label

onready var myCubeScrnLoc = get_node("../Camera").myCubeScrnLoc

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	myCubeScrnLoc = get_node("../Camera").myCubeScrnLoc
	myCubeScrnLoc = myCubeScrnLoc	
        #^This doesnt help. The value still doesn't change.

	print("LableNode: myCubeScrnLoc equals: ", myCubeScrnLoc)
    	#^This does't change as I move the cube, even though it's under func _process(delta).

	set_position(myCubeScrnLoc)

P.S. I also tried putting the label node underneath the kinematic one. It doesn’t work. Maybe because the Label node isn’t even in the 3D world?

Would signals help?

P.P.S. sorry if this post has weird formatting. I guess I’m not used to Godot Q&A forum posting.

I would think that the value from unproject_position() would give the correct location of the cube on the Viewport. Maybe some node has to be set up or initialized in the _ready() function of the Label script?

Ertain | 2020-01-16 01:36

What is setting up or initializing a node? I’m not sure what to do here.

2nd Edit: I tried using a Viewport node, but it’s just like the label node.
When the Viewport node tries to get the screen position from the camera node,
it doesn’t change when the cube moves around.

Would signals or global variables help?

Sdhy | 2020-01-16 03:14

It may be easier to write something like this in the Label node’s script:

# Replace these with paths to the respective nodes in the scene tree.
onready var camera = $path/to/camera_node
onready var cube = $path/to/cube_node

func _process(delta):
     var cube_position = camera.unproject_position(cube)
     set_position(cube_position)

Ertain | 2020-01-16 07:42

:bust_in_silhouette: Reply From: Sdhy

Thanks, Ertain! Your simple script worked!
It also means all the code can fit on a single label node,

I just had to change the variable for unproject_position to a Vector3 variable (otherwise, it woudn’t work).
I also changed some variable names.

The text appears beside the cube, so I’ll have to figure out how to make it apear above, instead.

For anyone else who wants to know, below is a copy of the modified script that now makes the label node follow the cube character.
Copy and paste this into a Label node script:

extends Label

#This lets you get the character-cube's 3D location:
onready var cube = get_node("../Cube_KinematicBody")

#This lets you use a function that turns the cube's 3D location into a 2D location on your screen:
onready var camera = get_node("../Camera")

#We need to set a blank 3D Vector variable:
var cubeLoc = Vector3(0,0,0)	
    
#Everything under this func _process(delta) is "updated" every frame:
func _process(delta):
	#We want to get the cube's 3D location at each frame.
	#We need to do this once for each coordinate.
	#We'll use the 3D Vector we defined earlier:
	cubeLoc.x = cube.get_global_transform().origin.x
	cubeLoc.y = cube.get_global_transform().origin.y
	cubeLoc.z = cube.get_global_transform().origin.z

	#We need to convert the character-cube's 3D location into a 2D coordinate on your game-window's screen and set it to a variable:
	var label_pos = camera.unproject_position(cubeLoc)
	
	#And finally, we set this label node (i.e. the RPG dialogue) to that spot on the game-screen:
	set_position(label_pos)

Notes:

  • The label node is on the same level as the camera and cube nodes.
  • You can use the kinematicBody node for the cube’s 3D location. You don’t have to use whatever mesh node is underneath it.
  • Kinematic movement code not included. You can find a YouTube tutorial.
  • I’m not sure how to make the label actually appear above the cube. But at least it now follows the cube!

It’s just what I was looking for but you got too complicated with the script, it can be greatly simplified and I’ll explain how:

The label is a child of a 3D node, right? Let’s say a KinematicBody, so there’s no need to create a script for the label, we’ll use the same KinematicBody script and reduce everything to 2 lines:

func _process(delta):
	var label_pos = camera.unproject_position(global_transform.origin)
	get_node("Label").set_position(label_pos)

Why is it better this way? Because we take directly the global_transform.origin of the character without the need to create variables that match values etc.

What do you want to change the position of the label? It’s simple, let’s say the label is 200 wide, and we want it to appear above the character’s head, which we don’t know exactly how big it is, so we do the following:

func _process(delta): 
	var label_pos = camera.unproject_position(global_transform.origin)
	get_node("Label").set_position(Vector2(label_pos.x - 100, label_pos.y - ?????))

Where I put ??? is a matter of testing numbers until it is exactly where you want it.

Javier Ocampos | 2020-08-13 17:52