3
\$\begingroup\$

I'm struggling with a persistent problem in my Godot 4.4.1 project involving "power slots" arranged around a table or arena background. No matter how I structure my nodes and update my code, I keep running into these problems:

  • Power slot nodes and cards overlap or clump near the center of the table instead of spacing out neatly around the oval.
  • Card sprites sometimes appear under the table background or UI, instead of in front.
  • Attempts to fan cards horizontally in each slot either result in them stacking, rotating oddly, or being invisible.
  • Debug prints confirm my slot node names and setup are correct, but cards/cards_container layering and arc math never work as intended.

I've reviewed tutorials and forum posts, but can't get reliable results. I've cleared the .godot cache, double-checked z-indexes, and strictly parented all power slots to a Node2D container. Still, the arc arrangement and card stacking issues persist.

Each player will have 3 cards.

What I'm Trying To Achieve

  • Multiple "agent/power slots" should be evenly distributed in an arc around an oval or arena.
  • Each slot displays an avatar, name, and a fan of power cards, spaced out, not overlapping or hidden.
  • Cards must always show above the table background and UI.

What I've Already Tried

  • Parent all slots under a Node2D container, not a Control or Panel.
  • Calculate arc positions and set slot rotation so each "looks in" or stays upright.
  • Use z_index and set it high for slots and cards.
  • Manually position/fan out TextureRects for cards under each slot node (inside a Node2D or Control).
  • Check that slot and card node names/types match the script.

Problems Still Present

  • Slots/cards still overlap at the center or don't follow the arc at all.
  • Sometimes cards appear below the background or UI, despite z_index changes.
  • When fanning three cards, they may stack or rotate incorrectly, even with custom code.
  • No runtime or inspector errors, only persistent render issues and layout bugs.

Questions

  1. What are the most reliable ways in Godot 4 to keep Node2D-based slots distributed in an exact arc/ellipse—especially when referencing a Control or Panel background for arc math?
  2. How can I guarantee cards/cards_container appear visually above all background or UI, no matter what?
  3. Is there a trick for getting fanned cards to always appear at the right position, not stacking or floating off-screen?
  4. What are common gotchas with z-index, CanvasLayer, or parent/child setup that cause cards or slots to vanish or overlap?

Current Scene Structure

MainScreen (Node2D) ├── Background (TextureRect/Panel) # Arena visual ├── PowerSlotContainer (Node2D) │ ├── PowerSlot1 (Node2D) │ └── PowerSlot2 (Node2D) │ └── SlotLayout (VBoxContainer) │ ├── NameLabel (Label) │ ├── PowerLabel (Label) │ └── CardFan (Node2D) ├── CanvasLayer │ └── UI Controls/Panels 

Example Card Display Function (pseudo-GDScript)

for i in range(card_count): var card_sprite = TextureRect.new() # assign texture, set card_sprite.custom_minimum_size, etc. card_sprite.position.x = i * (card_width * (1 - overlap)) card_sprite.rotation_degrees = base_angle + (fan_angle * i / max(1, card_count - 1)) card_sprite.z_index = 25 card_fan.add_child(card_sprite) 

Still, this produces stacking, invisible, or under-table card visuals.

Screenshots/Wireframe

Wireframe shows 1 card but will be 3 cards in the game

What Am I Missing?

  • Is there a bug or change in Godot 4.4.1 that affects layering or Node2D–Control interaction?
  • Should I be doing the arc calculations in global or local space for best accuracy?
  • Is CanvasLayer/Control always drawn on top of Node2D, regardless of z-index?
  • Could Control-based containers (VBox/HBox) inside a Node2D slot cause unexpected stacking?

Any practical advice, working code samples, or best practices for slot arc/distribution and reliable 2D card fanning/layering in Godot 4 would be a huge help. Thank you!

Update as of July 26th:

  1. The Path (MainPlayScreen.tscn):

    • The main scene contains a Path2D node named TablePath.
    • This TablePath has a Curve2D resource where the desired layout (a "U" shape or arc) is visually drawn directly in the editor. This path defines the exact line that the player slots will follow.
  2. The Follower (PlayerSlot.tscn):

    • This scene represents a single player's UI area.
    • Its root node is a PathFollow2D, which is a special node designed to automatically stick to and move along a Path2D.
    • Inside, it contains a Node2D named CardFan which acts as a stable pivot point for the "card" graphics.

Code Logic Summary

1. mainplayscreen.gd (The "Conductor")

This script is responsible for creating the player slots and telling them where to sit on the path.

  • _arrange_players_on_path() function: This is the core layout function. It loops through all active players and for each one:

    1. Creates a new instance of PlayerSlot.tscn.
    2. Adds the new instance as a child of the TablePath node.
    3. Calculates a ratio (e.g., 0.0 for the first player, 0.1 for the second, etc.).
    4. Sets the progress_ratio property of the PlayerSlot (PathFollow2D) to this ratio. This is the key step that automatically moves the slot to the correct position along the visually drawn curve.
    # In mainplayscreen.gd func _arrange_players_on_path(): player_slot_nodes.clear() for child in table_path.get_children(): child.queue_free() var player_count = players.size() if player_count == 0: return for i in range(player_count): var slot = PlayerSlotScene.instantiate() slot.deck_node = deck_node table_path.add_child(slot) var ratio = float(i) / float(player_count) slot.progress_ratio = 1.0 - ratio // Spreads slots evenly slot.set_player_data(players[i]) player_slot_nodes.append(slot) 
  • display_hand() function: After dealing, this function finds the correct PlayerSlot instance and delegates the task of showing the cards to it. It does not handle any visual logic itself.

    # In mainplayscreen.gd func display_hand(player_to_display: Player, player_index: int): if player_slot_nodes.size() > player_index: var slot = player_slot_nodes[player_index] slot.display_cards(player_to_display.current_hand, ...) 

2. PlayerSlot.gd (The "Follower" and "Card Displayer")

This script is attached to PlayerSlot.tscn and is responsible for displaying the cards correctly. This is where the card sizing logic is located.

  • display_cards() function: This function receives the card data and is responsible for creating the visual card graphics.

    1. It calculates the desired card_size (e.g., a small Vector2(60, 84) for AI players).
    2. It loops through the card data and for each card, creates a new TextureRect node.
    3. Crucially, it attempts to control the size of the TextureRect using three properties to force the large card texture to shrink down and fit into the small card_size rectangle.
    # In PlayerSlot.gd func display_cards(cards_array: Array, show_faces: bool, is_ai: bool): for c in card_fan.get_children(): c.queue_free() var card_size = Vector2(80, 112) if not is_ai else Vector2(60, 84) for i in range(cards_array.size()): var sprite = TextureRect.new() sprite.texture = _get_card_texture(...) # --- This is the logic that is intended to control the card size --- sprite.custom_minimum_size = card_size sprite.expand_mode = TextureRect.EXPAND_KEEP_SIZE sprite.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED # ----------------------------------------------------------------- # This logic creates the fanned effect # ... (code for position and rotation) ... card_fan.add_child(sprite) 

enter image description here


Here's the scene view along with scene structure and screenshot:

enter image description here

MainPlayScreen Scene Structure:

enter image description here

enter image description here

PlayerSlot Scene Structure:

enter image description here

In-game screenshot:

enter image description here

\$\endgroup\$
3
  • 1
    \$\begingroup\$ As a tip, you don't need to separate and date your edits. The StackExchange system handles tracking edit history automatically, so anyone who's curious can see exactly what was changed when. Instead, it's usually better to integrate your edits, so that the revised question reads the way it would if it contained all the relevant information from the first post. This is often easier and faster to read and understand than starting with an incomplete question and appending layers of extra clarifications at the end. \$\endgroup\$ Commented Jul 26 at 15:41
  • \$\begingroup\$ May I ask why you've removed the contents of your question? This kind of thing is normally considered a form of vandalism to be reverted by the moderators, but I wanted to check with you first to see if there might be a solution better-suited to this situation. For example, if you're concerned about intellectual property rights for content in the question, we may be able to edit the post to still be intelligible without showing anything proprietary. As a mod, I can also help redact the edit history, if there was something present there that you don't want visible. \$\endgroup\$ Commented Aug 16 at 23:20
  • \$\begingroup\$ I went ahead and rolled it back to the helpful version of the question. As @DMGregory said, if you are looking for an alternative solution, please feel free to reach out. \$\endgroup\$ Commented Aug 18 at 17:01

1 Answer 1

4
\$\begingroup\$

I'm not sure I fully understood your design. However, your issue seemed pretty clear, so here's my answer: I address a simple case where players sit around a table and hold cards — think Texas Hold 'Em.

  1. What are the most reliable ways in Godot 4 to keep Node2D-based slots distributed in an exact arc/ellipse (...)?

You could use a Curve2D to sample positions along a curve. However, an approach I prefer when such a curve is a game-world trajectory is to create a Path2D and use a child PathFollow2D to sample positions on it:

Path2D with a child PathFollow2D representing the player.

Having each player be a PathFollow2D, we can space them out evenly by telling them how far to move from the head of the table:

@onready var _edge: Path2D = get_node("Table/Edge") func sit_players() -> void: var player_count: int = _edge.get_child_count() for player_idx: int in _edge.get_children(): var ratio: float = player_idx / float(player_count) _edge.get_child(player_idx).progress_ratio = ratio 

(...) especially when referencing a Control or Panel background for arc math?

Control nodes are intended for building user interfaces and scaling up with the window size. They behave poorly when used as a plain replacement for Node2Ds, as their transforms behave differently.

This is why I'm using Node2Ds and working in world space rather than screen space: I have more control (no pun intended) over their position and movement, regardless of screen resolution or camera zoom.

  1. How can I guarantee cards/cards_container appear visually above all background or UI, no matter what?

Child nodes always draw in front of their parent. They also draw behind or in front of sibling nodes depending on tree order and Z-index. Add cards as children of the player avatar and sort them as you need:

Player holding cards.

  1. Is there a trick for getting fanned cards to always appear at the right position, not stacking or floating off-screen?

Children nodes always move along with their parent due to transform composition. If you want more control of their position in local space, you can use an additional Node2D as a pivot point:

Additional Node2D for creating a card fan.

What are common gotchas with z-index, CanvasLayer, or parent/child setup that cause cards or slots to vanish or overlap?

  1. Z-index: use it to change the draw order of the current node relative to its parent. If you want to set an absolute Z-index, disable z_as_relative.
  2. CanvasLayer: you can change its layer to control draw order when using many layers.
  3. Parent/child setup: see (1) above and the answer to your second question.

From the comments:

I followed your steps but I now see cards taking over the entire screen.

When TextureRect.expand_mode is set to ExpandMode.EXPAND_KEEP_SIZE, the node's minimum size equals the texture's size. If your texture is huge, it will cover most of the screen.

You can solve this by selecting a different expand mode, or changing the node transform's scale. However, I still suggest you switch to a Node2D to have more control over position, rotation, and scale: Sprite2Ds allow shifting the sprite's offset, whereas Controls don't.

\$\endgroup\$
5
  • \$\begingroup\$ Thank you so much for such a detailed answer. Really means a lot! I will try this solution and update you on the progress. \$\endgroup\$ Commented Jul 26 at 10:31
  • \$\begingroup\$ Hi. I followed your steps but I now see cards taking over the entire screen. I have modified the details above. Thank you! \$\endgroup\$ Commented Jul 26 at 13:21
  • 1
    \$\begingroup\$ Could you please share your new scene structure and node setup in the inspector? Size issues are likely related to object transforms and hierarchy \$\endgroup\$ Commented Jul 26 at 13:49
  • \$\begingroup\$ Hi. I have added the screenshots outlining the structure of scenes and other details. \$\endgroup\$ Commented Jul 27 at 14:18
  • \$\begingroup\$ I updated my answer to address your latest issue. OTOH, I still reckon you are doing yourself a disservice by using Controls over Node2Ds: don't force nodes to do things they are not supposed to do; instead, pick the correct node for the job and make your life easier. I also address this aspect briefly in my answer. \$\endgroup\$ Commented Jul 30 at 8:14

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.