Scenario: I have 3D boards that are round and divided into rings and columns. Think of a dartboard laid flat - similar in segmentation but my boards are larger and have far more segments. The player will be viewing the boards from an isometric perspective. Characters will occupy the cells on these boards. I already have an input_event signal attached to select the clicked character. I also want to allow players to be able to click the cell that a character occupies and/or click another cell to indicate moving a character to that cell. Think something akin to how most 3D chess games work. You can click on the pawn or the square that the pawn is in and then click on another square to move that pawn to.

As I build the boards and populate the game, I also map the board cells. So I know the Vector3 positions of all four corners of each cell. I'd like to create invisible click areas that are distinct to the cell they are attached to, but I can't necessarily go into my scenes in the 3D editor and manually attach these without it taking a long, long time. There is also the likelihood that the boards may change as the game progresses or as new maps / levels are added. My question is a 2 parts.

  1. What would be the recommended approach to creating these clickable objects on the fly through GDScript?
  2. How would I constrain the boundaries of each clickable tile to the four corners for the cell?

You could loop through them, child StaticBody3Ds to them, use the Vector3s to create ConcavePolygonShape3Ds, and then hook up to StaticBody3D's input signals for whatever you need to do with them.

    award Now that is a clever solution. I'll give that a shot sometime over the weekend if I have time and let you know how it goes. Thanks.

    Plain 3d areas with concave colliders may be sufficient. You don't really need static bodies if the only purpose is to capture mouse clicks.

    I think I may be doing something wrong. The mouse event doesn't seem to be captured.

    So I have a generic class that I instance for each cell in the board and store in a dictionary (for reference throughout the rest of the game)

    extends Node3D
    class_name BoardCell
    
    var topLeft: Vector3;
    var topRight: Vector3;
    var bottomLeft: Vector3;
    var bottomRight: Vector3;
    
    func _init(TR: Vector3, BR: Vector3, TL: Vector3, BL: Vector3) -> void:
    	topRight = TR;
    	topLeft = TL;
    	bottomRight = BR;
    	bottomLeft = BL;
    	_attachCollisionArea();
    
    func _attachCollisionArea() -> void:
    	var area = Area3D.new();
    	var coll = CollisionShape3D.new();
    	var poly = ConcavePolygonShape3D.new();
    	poly.set_faces([topRight, topLeft, bottomLeft, bottomRight]);
    	coll.shape = poly;
    	area.add_child(coll);
    	area.input_event.connect(_on_area_3d_input_event)
    	self.add_child(area);
            # I also tried self.add_child.deferred(area);
    
    func _on_area_3d_input_event(_camera, event, _position, _normal, _shape_idx):
    	if event is InputEventMouseButton:
    		if event.button_index == 1 && event.pressed == true:
                            # Nothing happens here			
                            print(self);

    Am I missing something?

    • xyz replied to this.

      damonk Let's see the instancing code as well.

      Sure.

      This fires up the board builder. Just creates the "dartboard" like structure.

      extends Node
      class_name BoardBuilder
      
      # This class is responsible for building individual boards for the game
      # in a virtual sense. The "virtual board" contains arrays of board cells by 
      # rank and file with each board cell containing information specific to the cell.
      # This information includes the corner boundaries in Vector3 format, the center
      # (centroid) position of each cell in Vector3, whether or not a cell is occupied
      # by a piece and if so, a reference to the actual piece occupying the cell.
      
      const BoardRank := preload("res://scripts/Logic/boardRank.gd");
      const BoardCell := preload("res://scripts/Logic/boardCell.gd");
      # Inner ring. The bulls-eye if you will.
      const ASCENSION_RING_RADIUS: float = 2.0;
      const BOARD_THICKNESS: float = 0.075;
      const BOARD_SEPARATION: int = 3;
      
      # Identifies the board we're working with
      var BoardIndex: int;
      # Contains the ranks
      var Ranks: Array[BoardRank] = [];
      
      func _init(board: int, ranks: int, files: int) -> void:
      	# Set our board index
      	BoardIndex = board;
      	# For each rank, create a Rank object.
      	for r in range(ranks):
      		_createRank(r, files);
      
      func _createRank(_rank: int, _files: int) -> void:
      	var boardY = (BoardIndex * BOARD_SEPARATION) + BOARD_THICKNESS;
      	var _inner = _plotRingVertices(_files, _rank, Vector3(0, boardY, 0));
      	var _outer = _plotRingVertices(_files, _rank + 1, Vector3(0, boardY, 0));
      	Ranks.append(BoardRank.new(_inner, _outer));
      
      func GetRank(rank: int) -> BoardRank:
      	return Ranks[rank];
      
      func _plotRingVertices(columns: int, radiusFromCenter: int, pivotPoint: Vector3) -> Array[Vector3]:
      	var _v: Array[Vector3] = [];
      	var slice = 2 * (PI / columns);
      	for _c in columns:
      		var angle = slice * _c;
      		var newX = (pivotPoint.x + radiusFromCenter + ASCENSION_RING_RADIUS) * cos(angle);
      		var newZ = (pivotPoint.z + radiusFromCenter + ASCENSION_RING_RADIUS) * sin(angle);
      		_v.append(Vector3(newX, pivotPoint.y, newZ));
      	return _v;

      This builds the individual rings or ranks for the board which creates the distinct BoardCell objects.

      extends Node
      class_name BoardRank
      
      const BoardCell := preload("res://scripts/Logic/boardCell.gd");
      
      var _innerVertices: Array[Vector3];
      var _outerVertices: Array[Vector3];
      var _cells: Array[BoardCell] = [];
      
      func _init(innerV: Array[Vector3], outerV: Array[Vector3]) -> void:
      	_innerVertices = innerV;
      	_outerVertices = outerV;
      	_generateCells();
      
      func _generateCells() -> void:
      	for _cell in range(_innerVertices.size()):
      		var leftSide: int = _cell;
      		var rightSide: int = _cell + 1;
      		if (rightSide == _innerVertices.size()):
      			rightSide = 0;
      		_cells.append(BoardCell.new(
      			_innerVertices[rightSide],
      			_outerVertices[rightSide],
      			_innerVertices[leftSide],
      			_outerVertices[leftSide]
      		));
      
      func GetCell(cell: int) -> BoardCell:
      	return _cells[cell];
      • xyz replied to this.

        damonk When you create a new node you need to add it to the scene tree. Otherwise it won't be processed or drawn in the main game loop. This is typically done by adding it as a child to a node that's already in the tree:

        var cell = BoardCell.new(a, b, c, d)
        some_node_in_the_scene_tree.add_child(cell)

          xyz Doh! I knew there was something I was missing. Let me give that a shot.

          • xyz replied to this.

            damonk Also have in mind that Godot's nodes effectively have 3 "constructors":
            _init() - called when node object is created. This is the constructor in the strict sense
            _enter_tree() - called when node is added to the scene tree
            _ready() - called when node and all of its children have finished entering the tree, and the _ready() of all of node's children has been executed.

            @xyz @award Thank you both for the tips. Got it working like a champ now.