You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

同预制体卡牌点击异常:点击牌堆后首个Tableau卡牌失效问题排查

类纸牌游戏交互异常问题

我正在开发一款类纸牌游戏,所有卡牌均基于同一Prefab创建。Tableau区域的所有卡牌运行完全相同的脚本,但首个Tableau的首张卡牌仅在特定场景下可被点击。
我曾排查过Z轴位置相关问题,已使用Sorting Layers,但添加Z轴设置后问题仍未解决,故已移除该配置。
核心问题:点击牌堆卡牌后,首个Tableau的首张卡牌会被判定为“不存在”,被当作空Tableau处理,任何King(或本游戏中的Ace)都能放到它的位置;但未点击牌堆按钮时,该卡牌可正常交互。无法理解点击牌堆为何会触发此问题。

用户输入交互代码

public class UserInput : MonoBehaviour
{
    public GameObject slot1;//Already selected card
    private Reset reset;
    private Game game;
    public float zOffSet = 0.5f;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        reset = FindAnyObjectByType<Reset>();
        game = FindAnyObjectByType<Game>();
        slot1 = this.gameObject;
    }

    // Update is called once per frame
    void Update()
    {
        GetMouseClick();
    }
    void GetMouseClick()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Vector3 mousePosition = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Input.mousePosition.z));
            //See if it hits anything
            Collider2D hit = Physics2D.OverlapPoint(Camera.main.ScreenToWorldPoint(Input.mousePosition));
            if (hit)
            {
                if (hit.CompareTag("Card"))
                {
                    CardHit(hit.gameObject);
                }
                else if (hit.CompareTag("Tableaus"))
                {
                    TableauHit(hit.gameObject);
                }
            }
        }
    }
    void CardHit(GameObject selected)
    {
        //If face down & not blocked, flip it
        if (!selected.GetComponent<Selectable>().faceUp)
        {
            if (!Blocked(selected))
            {
                selected.GetComponent<Selectable>().faceUp = true;
                slot1 = this.gameObject;
            }
        }
        //If face up and no card selected already, select it
        if(selected.GetComponent<Selectable>().faceUp)
        {
            if (slot1 == this.gameObject)
            {
                slot1 = selected;
            }
            //If card selected already (and it's not the same) & new card can go on it, move it. Simply select new card only if not.
            else if (slot1 != selected)
            {
                if (IsValid(selected))
                {
                    //Gain 2 points for valid card placement
                    game.score += 2;
                    PlaceCard(selected);
                }
                else
                {
                    slot1 = selected;
                }
            }
        }
    }
    void TableauHit(GameObject selected)
    {
        if (slot1.CompareTag("Card"))
        {
            if (slot1.GetComponent<Selectable>().value == 13 || slot1.GetComponent<Selectable>().value == 1)//In this game, can place either Aces or Kings as the top value
            {
                PlaceKingOrAce(selected);
            }
        }
    }

    bool IsValid(GameObject selected)
    {
        bool cardValid = false;
        Selectable s1 = slot1.GetComponent<Selectable>();//Selectable script from slot 1
        Selectable s2 = selected.GetComponent<Selectable>();//Selectable script from the recently selected game obj

        if (!s2.inDeck)
        {
            if(s1.suit==s2.suit)
            {

                if (s1.value == (s2.value - 1))//the number of card in play is one less than and the same suit as the card they want to place it on
                {
                    cardValid = true;
                }
                else if (s1.value == 13 && s2.value == 1)//Kings can go on top of Aces
                {
                    cardValid = true;
                }
            }
        }

        return cardValid;
    }

    void PlaceCard(GameObject selected)
    {
        Selectable s1 = slot1.GetComponent<Selectable>();
        Selectable s2 = selected.GetComponent<Selectable>();
        float yOffSet = 0.5f;

        slot1.transform.position = new Vector3(selected.transform.position.x, selected.transform.position.y - yOffSet);//, selected.transform.position.z - zOffSet);
        slot1.transform.parent = selected.transform; //Makes them move with parent cards
        slot1.GetComponent<Renderer>().sortingOrder = (slot1.transform.parent.GetComponent<Renderer>().sortingOrder) + 1;//One higher than the prior card's sorting order


        //Remove card from the deck, if it was in there
        if (s1.inDeck)
        {
            game.deck.Remove(slot1.name);
        }
        //Remove from the tableau it was in, if it was in one
        else
        {
            game.tableaus[s1.row].Remove(slot1.name);
        }

        s1.inDeck = false;//Can't add a card into deck
        s1.row = s2.row;

        //"Reset" slot1
        slot1 = this.gameObject;

        //Increase zOffset
        zOffSet += 0.5f;
    }

    void PlaceKingOrAce(GameObject selected)
    {
        Selectable s1 = slot1.GetComponent<Selectable>();
        Selectable s2 = selected.GetComponent<Selectable>();

        slot1.transform.position = new Vector3(selected.transform.position.x, selected.transform.position.y);//, selected.transform.position.z);
        slot1.transform.parent = selected.transform; //Makes them move with parent cards

        //Remove card from the deck, if it was in there
        if (s1.inDeck)
        {
            game.deck.Remove(slot1.name);
        }
        //Remove from the tableau it was in, if it was in one
        else
        {
            game.tableaus[s1.row].Remove(slot1.name);
        }

        s1.inDeck = false;//Can't add a card into deck

        //"Reset" slot1
        slot1 = this.gameObject;
    }

    bool Blocked(GameObject selected)
    {
        Selectable s2 = selected.GetComponent<Selectable>();

        if (s2.name == game.tableaus[s2.row].Last())//Check it's the bottom card
        {
            return false;
        }
        else
        {
            return true;
        }
    }
}

发牌逻辑代码

public void DealFromDeck()
{
    float xOffset = 1.7f;
    string card;
    UpdateSprite[] allCards = FindObjectsOfType<UpdateSprite>();
    if (deckLocation < (deck.Count))//can't increment it if at end of deck
    {
        card = deck[deckLocation];
    }
    else//Reset when at end of deck
    {
        deckLocation = 0;
        card = deck[deckLocation];
        //Erase deck button children
        foreach(UpdateSprite allCard in allCards)
        {
            if(allCard.transform.parent != null)
            {
                if (allCard.transform.parent.CompareTag("DeckButton"))
                {
                    Destroy(allCard.gameObject);
                }
            }
        }
    }
    GameObject newCard = Instantiate(cardPrefab, new Vector3(deckButton.transform.position.x + xOffset, deckButton.transform.position.y), Quaternion.identity, deckButton.transform);
    newCard.GetComponent<Renderer>().sortingOrder = deckLocation+1;
    newCard.transform.localScale = new Vector3(15, 15, 0);
    newCard.name = card;
    newCard.GetComponent<Selectable>().faceUp = true;
    deckLocation++;
    deckZOffset += 0.3f;
}

问题排查与修复方案

1. 修复点击检测的Z轴偏移问题

2D物理检测中,屏幕坐标转世界坐标时需固定Z轴为相机近平面,否则检测点可能不在正确的交互平面上。修改GetMouseClick方法:

void GetMouseClick()
{
    if (Input.GetMouseButtonDown(0))
    {
        // 固定Z轴为相机近平面,确保检测点在2D交互层
        Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane));
        Collider2D hit = Physics2D.OverlapPoint(mouseWorldPos);
        if (hit)
        {
            if (hit.CompareTag("Card"))
            {
                CardHit(hit.gameObject);
            }
            else if (hit.CompareTag("Tableaus"))
            {
                TableauHit(hit.gameObject);
            }
        }
    }
}

2. 修正Blocked方法的逻辑漏洞

原方法依赖game.tableaus的字符串匹配判断可点击性,发牌后可能因数据同步问题出错。改为直接通过层级结构判断是否为Tableau最顶层卡牌:

bool Blocked(GameObject selected)
{
    Transform parentTableau = selected.transform.parent;
    // 直接判断当前卡牌是否是Tableau的最后一个子物体(最顶层可交互卡牌)
    return selected.transform != parentTableau.GetChild(parentTableau.childCount - 1);
}

3. 解决发牌时的Sorting Order冲突

发牌时确保新牌的Sorting Order高于所有已有卡牌,避免被Tableau的Collider覆盖点击:

// 替换发牌代码中的sorting order设置逻辑
int maxSortingOrder = 0;
Renderer[] allRenderers = FindObjectsOfType<Renderer>();
foreach(Renderer r in allRenderers)
{
    if(r.sortingOrder > maxSortingOrder)
    {
        maxSortingOrder = r.sortingOrder;
    }
}
newCard.GetComponent<Renderer>().sortingOrder = maxSortingOrder + 1;

4. 补全TableauHit的空Tableau判断

添加逻辑判断Tableau是否已有卡牌,避免将King/Ace放到已有卡牌的Tableau上:

void TableauHit(GameObject selected)
{
    if (slot1.CompareTag("Card"))
    {
        Selectable slot1Sel = slot1.GetComponent<Selectable>();
        // 检查Tableau是否为空
        bool isTableauEmpty = selected.transform.childCount == 0;
        if ((slot1Sel.value == 13 || slot1Sel.value == 1) && isTableauEmpty)
        {
            PlaceKingOrAce(selected);
        }
    }
}

内容的提问来源于stack exchange,提问作者shannon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 19:57:03