+3 votes

Hey Godot community,

Over the last few weeks I've been very impressed with how intuitive everything is.

Currently I'm implementing a simple 3D runtime building system with which you can :
1. Select an object to place from a GUI Toolbar
2. Click on a tile within a different TileMap to place the selected object on top of that tile

This current code has no errors, and seems to work correctly when I check values in debugger but nothing happens when I try to place an Object.

func _process(delta):
if Input.is_mouse_button_pressed(BUTTON_LEFT) and buildModeOn :
    mousePosition = get_viewport().get_mouse_position()
    clickLocation = world_to_map(Vector3(mousePosition.x, mousePosition.y, 1))
    set_cell_item(clickLocation.x, clickLocation.y, 3, selectedObject, 1)


if Input.is_mouse_button_pressed(BUTTON_RIGHT) and buildModeOn :
    buildModeOn = false func _on_Lathe_gui_input(event):
if event is InputEventMouseButton and event.button_index == BUTTON_LEFT and event.pressed:
    var objLathe = 5
    selectedObject = objLathe
    buildModeOn = true

Ingame Pic
sceneTree

Anyone else who has built a building system with 3D tilemap that can assist?

Thanks in advance,
Olivier

asked Nov 3, 2019 in Engine by olivierwierda (15 points)

No answers yet unfortunately, if anyone can help me with this I'll post a detailed write-up on Reddit to show how to do it for other game devs too!

If you've not got it yet, an extension to the previous answer. As previous says, you will need to add colliders by Mesh->Mesh tab center top ish screen->create convex static body.
Then re-convert and re-add the new meshlib to your gridmap.

I believe your debug results are a false positive. Your result is the viewport mouse position converted to a map, so I think you are placing your objects in a space relative to the viewport.
We want to convert the object collider selected to a map coord, so we can place relative to the existing object.
You were close in the direction of thinking, but to select an object in such a way, with an event, requires a collision(or an expensive pathfinding loop) afaik.
So we use rays and colliders.
Then, here is a bit more complete extension of the previous answer, with some basic explanation of what the methods are doing.

extends GridMap

const raylength = 4444 #Arbitrarily large ray 
# warning-ignore:unused_argument
func _input(event):
    if Input.is_mouse_button_pressed(BUTTON_LEFT):
        var mouse_pos = get_viewport().get_mouse_position() #gets mouse position
        var from = $".."/Camera.project_ray_origin(mouse_pos) #create a ray origin at the mouse position
        var to = from + $".."/Camera.project_ray_normal(mouse_pos) * raylength #Extend the ray by raylength
        var space_state = get_world().direct_space_state #Gets AABB/Physics information
        var result = space_state.intersect_ray(from, to, [], 1) #Get ray intersection object, exclude colliders except layer 1 - this is our object
        if result:
            var selection = world_to_map(Vector3(result.position.x, result.position.y, result.position.z)) #Convert result object position to gridmap coordinate
            print(selection)

Now it should be the proper map coordinate of the actual object/tile. So now we'll be placing objects relative to the tile's map position, rather than the viewport.
(1, 0, 4)
(1, 0, 3)
(0, 0, 3)

1 Answer

0 votes

First of all is in the line:
set_cell_item(clickLocation.x, clickLocation.y, 3, selectedObject, 1) you always send 3 as the Z coordinate. In Godot the Y axis is up so unless your grid is oriented "up" and you are certain you want to work on the third level of the grid map you can replace your code to this one set_cell_item(clickLocation.x, 3, clickLocation.y, selectedObject, 1)
If however your grid is oriented up there is still two problems with the way you determine where the user have clicked on your grid. the mouse position in the viewport is an absolute value that is not correlated to the scene the 3D camera is looking at.
In order to get the correct grid cell the user is pressing on you need to send a ray from the camera through the mouse position in the viewport and test where does the ray intersect and with what.
The first way of doing it is adding for each of your meshes in the MeshLib a collision body like seen here. then when the user clicks the mouse run this code to get the object you clicked on:

# cast a ray from camera at mouse position, and get the object colliding with the ray
func get_object_under_mouse():
    var mouse_pos = get_viewport().get_mouse_position()
    var ray_from = $Camera.project_ray_origin(mouse_pos)
    var ray_to = ray_from + $Camera.project_ray_normal(mouse_pos) * RAY_LENGTH
    var space_state = get_world().direct_space_state
    var selection = space_state.intersect_ray(ray_from, ray_to)
    return selection

After you have the object the user clicked on convert the objects translation to grid coordinates and then you can remove the magic 3 number from your code and replace it with the coordinates you get.
The second method is a little harder to understand and maintain but you can use it if for some reason you don't want collision on your meshes.
Instead of finding any intersection the ray has with the world you can test the intersection of the ray with a Plane object. Assuming that the center of meshes in your GridMap has Y value of zero you can create a plane with Y value zero like so var plane = Plane(Vector3.UP, 0) and then replace the lines:

var space_state = get_world().direct_space_state
var selection = space_state.intersect_ray(ray_from, ray_to)

With this line that calculates the 3D coordinate in the world where you ray intersects with the plane:

var intersection = plane.intersects_ray (ray_from, ray_to )

I would recommand the first approch especilly if you are just prototyping because you can get it to work in a couple of minutes.

Good luck, hope this help you.

answered Nov 5, 2019 by Barel (14 points)
Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.