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

Carcassonne卡牌游戏道路计分函数随机重复调用问题求助

《卡卡颂》桌游开发问题:道路检测函数重复触发排查

我正在开发桌游《卡卡颂》(Carcassonne),使用预制卡牌,每张卡牌用s、v、j、z四个变量对应本国语言的方位体系。我编写了函数识别卡牌上标记为"R"(代表道路)的边,并查找相邻卡牌——比如当s=="R"时,会调用第二个函数查找上方卡牌,同时设置lastSide为"j"避免回溯。由于道路只会出现在卡牌的两个边上,所以设置了变量nicovani。

目前遇到的问题:有时放置卡牌后,函数会先从刚放置的卡牌触发一次调用,接着从之前放置的卡牌触发一次,随后又从刚放置的卡牌再次触发,无法定位问题根源,这是完成游戏前最后要解决的问题。

相关代码

public string s;
public string v;
public string j;
public string z;
private int cross = 0;
public bool Layed;
public bool IsRoadEnding;
private string lastSide;
private int nicovani = 0;
private bool isScored = false;

public void OnMouseDown()
{
    if(Layed == false)
    {
        if(r == 1)
        {
            r = 2;
            IsHere = false;
            StartCoroutine(Follow());
        }
        else 
        {
            if(IsHere == true)
            {
                TheWholeThing();
            }
            else
            {
                r = 1;
                transform.position = spawner.transform.position;
            }
        }
    }
}

void TheWholeThing()
{
    setPos = new Vector2 (Mathf.RoundToInt(transform.position.x), Mathf.RoundToInt(transform.position.y));
    r = 1;
    transform.position = setPos;
    FindTile();
    CheckTile(asociatedTile);

    if(r == 1)
    {
        drawer.SpawnCard();

        SetTile(asociatedTile);
        
        gmg.GenerateGrid(transform.position.x, transform.position.y+1, "j" , s);
        gmg.GenerateGrid(transform.position.x, transform.position.y-1, "s", j);
        gmg.GenerateGrid(transform.position.x +1, transform.position.y, "z", v);
        gmg.GenerateGrid(transform.position.x -1, transform.position.y, "v", z);
    
        Layed = true;

        startingTile = gameObject.transform;
        if(nicovani < 2)
        {
            FindScoringRoad(transform.position.x, transform.position.y, "", s, v, j, z);
            return;
        }
    }
    else
    {
        return;
    }

    IsHere = false;
}

void FindScoringRoad(float x, float y, string side, string s, string v, string j, string z)
{
    if(isScored == false)
    {
        lastSide = side;

        if(lastSide != "s")
        {
            if(s == "R")
            {
                cross = 0;
                Debug.Log("s");
                FindNextCard("j", x, y + 1);
            }
        }

        if(lastSide != "v")
        {
            if(v == "R")
            {
                cross = 0;
                Debug.Log("v");
                FindNextCard("z", x + 1, y);
            }
        }

        if(lastSide != "j")
        {
            if(j == "R")
            {
                if(nicovani == 2)
                {
                    nicovani = 0;
                    return;
                }
                else
                {
                    cross = 0;
                    Debug.Log("j");
                    FindNextCard("s", x , y - 1);
                }
            }
        }
      
        if(lastSide != "z")
        {
            if(z == "R")
            {
                if(nicovani == 2)
                {
                    nicovani = 0;
                    return;
                }
                else
                {
                    cross = 0;
                    Debug.Log("z");
                    Debug.Log(x + " " + y);
                    FindNextCard("v", x - 1, y);
                }
            }
        }
        cross = 0;
        return;
    }
}

void FindNextCard(string side, float x, float y)
{
    if(x == startingTile.position.x && y == startingTile.position.y)
    {
        Debug.Log("Road Closed");
        isScored = true;
        return;
    }

    foreach(GameObject card in drawer.spawnedCards)
    {
        if(card.transform.position.x == x && card.transform.position.y == y)
        {
            var cardS = card.GetComponent<Card>();
            if(cross < 1)
            {
                FindScoringRoad(card.transform.position.x, card.transform.position.y, side, cardS.s, cardS.v, cardS.j, cardS.z);
                cross++;
            }          
            return;
        }         
    }

    Debug.Log("Ends here");
    cross = 0;
    nicovani++;
    return;
}

问题根源分析

  1. 实例变量状态冲突:nicovani、cross、isScored、lastSide都是Card类的私有成员,每个卡牌实例都有独立的这些变量。递归调用相邻卡牌的FindScoringRoad时,会切换到相邻卡牌的实例变量,导致不同卡牌的状态互相干扰,触发重复调用。
  2. 起始位置覆盖问题:startingTile在新卡牌放置时会被赋值为当前卡牌的transform,旧卡牌的递归调用会使用这个新值,导致道路闭合判断逻辑混乱,引发重复检测。
  3. 回溯状态管理失效:lastSide是当前卡牌实例的变量,递归到相邻卡牌后无法保留原卡牌的lastSide状态,返回时可能重复检查同一方向,造成循环调用。

修复方案

1. 将递归状态参数化

把所有影响递归逻辑的状态变量(lastSide、nicovani、cross、isScored、起始位置)改成FindScoringRoad和FindNextCard的参数,让每次递归调用都携带独立的状态,避免不同卡牌实例的变量互相干扰。

修改后的核心函数示例:

// 修改FindScoringRoad,传递独立状态参数
void FindScoringRoad(float x, float y, string lastSide, string s, string v, string j, string z, int nicovani, bool isScored, Vector2 startPos)
{
    if(isScored) return;

    // 检查s方向道路
    if(lastSide != "s" && s == "R")
    {
        FindNextCard("j", x, y + 1, nicovani, isScored, startPos);
    }

    // 检查v方向道路
    if(lastSide != "v" && v == "R")
    {
        FindNextCard("z", x + 1, y, nicovani, isScored, startPos);
    }

    // 检查j方向道路
    if(lastSide != "j" && j == "R")
    {
        if(nicovani == 2) return;
        FindNextCard("s", x, y - 1, nicovani, isScored, startPos);
    }

    // 检查z方向道路
    if(lastSide != "z" && z == "R")
    {
        if(nicovani == 2) return;
        FindNextCard("v", x - 1, y, nicovani, isScored, startPos);
    }
}

void FindNextCard(string side, float x, float y, int nicovani, bool isScored, Vector2 startPos)
{
    // 判断是否回到起始位置(道路闭合)
    if(x == startPos.x && y == startPos.y)
    {
        Debug.Log("Road Closed");
        isScored = true;
        return;
    }

    foreach(GameObject card in drawer.spawnedCards)
    {
        if(card.transform.position.x == x && card.transform.position.y == y)
        {
            var cardS = card.GetComponent<Card>();
            // 传递当前状态到下一次递归
            FindScoringRoad(card.transform.position.x, card.transform.position.y, side, cardS.s, cardS.v, cardS.j, cardS.z, nicovani, isScored, startPos);
            return;
        }         
    }

    Debug.Log("Ends here");
    nicovani++;
    return;
}

// 在TheWholeThing中调用时传入初始状态
void TheWholeThing()
{
    // ... 原有代码 ...

    Layed = true;

    Vector2 startPos = transform.position;
    if(nicovani < 2)
    {
        FindScoringRoad(transform.position.x, transform.position.y, "", s, v, j, z, 0, false, startPos);
        return;
    }

    // ... 原有代码 ...
}

2. 移除Card类中的状态变量

删除Card类中不再需要的私有成员:cross、lastSide、nicovani、isScored、startingTile,避免这些变量残留引发其他问题。

3. 优化道路检测逻辑

可以考虑使用迭代代替递归,或者创建一个独立的RoadDetectionManager单例类,统一管理所有道路检测的状态,进一步避免实例变量冲突。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 09:30:58