25-02-13 回合制卡牌游戏开发记录


项目概述

项目的课程 Unity 中文课堂-《王国之梦》Roguelike 卡牌游戏开发教程

我已经完成的游戏在 B 站预览的视频:Unity 制作的类杀戮尖塔的回合制肉鸽卡牌游戏
游戏 Demo:《王国之梦》Roguelike 卡牌游戏 Demo

制作的内容及功能:

  • 随机地图生成
  • 随机房间生成
  • 三种敌人对战房间
  • 休息房间
  • 宝箱房间
  • 卡牌效果实现
    • 攻击牌
    • 防御牌
    • 回血牌
    • 抽卡牌
    • Buff 攻击力牌
    • DeBuff 攻击力牌
  • 对战胜利后随机抽卡
  • UI 动效
    • 血条动画及变色
    • 按钮反馈
    • 淡入淡出

学到的知识点:

  • UI Toolkit 制作全部 Runtime UI
    • USS 样式设计
    • Pseudo-classes
    • 数据绑定
  • 抽象类多态
  • Unity Event System
    • 拖拽非 UI 物体
  • Scriptable Object 事件传递
  • 通用泛型事件及监听
  • 数学逻辑算法
  • Unity 新 API
    • ObjectPool
    • Awaitable
  • Spine 动画
    • SDK 安装
    • 动画使用方法

项目开发

由于因为这个课程内容是收费的,在这篇公开的博客中,我就只记录一些在项目开发中遇到的问题和解决方案。

遇到的一些问题

1. UI Toolkit 绘制的 UI 图层渲染顺序和被上一层覆盖的 UI 元素仍然会被鼠标点击而触发 Clicked 绑定的事件。

  • 我是从这个项目彻底接触用 Unity UI Toolkit 来绘制游戏 Runtime 中的 UI,之前只是用 UIToolkit 绘制些简单的 Editor,导致刚上手的时候忘记在游戏运行中的渲染图层顺序,导致 UI 元素重叠,或者是被覆盖的 UI 元素仍然会触发 Clicked 绑定的事件,这并不是我想要的。

    比如我的项目中在实现展示玩家卡组所有卡牌的 Panel 时,左下角有一个取消返回的Button,是可以被触发点击事件的,但同时这个Button如果正好显示在地图房间上面,这个时候我点击,不仅会触发这个按钮上的 Clicked 事件也会触发地图房间上的事件,进行了加载场景。

    于是我尝试在 Room.cs 文件内添加了以下代码:

    // Room.cs
    private void OnMouseDown()
    {
        // 处理点击事件
        // Debug.Log("点击了房间" + roomData.roomType);
    
        if (EventSystem.current.IsPointerOverGameObject())
        {
            Debug.Log("点击到了UI");
            return;
        }
    
        if (roomStatsu == RoomStatus.Attainable)
        {
            loadRoomEvent.RaisEvent(this, this);
            GenerateRoomReward();
        }
    }

    EventSystem.current.IsPointerOverGameObject()方法可以判断当前鼠标是否点击到了 UI 元素,如果是则返回 true 直接 return,否则返回 false。这样就可以避免在点击卡牌时触发地图房间上的事件。

    也尝试在显示卡牌的 Panel 上试了加下面的代码,在 UI 元素注册点击事件的回调,并在点击时阻止事件冒泡,以此避免事件被父元素捕获:当点击这些元素时,事件不会继续向上传播到父元素的监听器,从而避免父元素的点击事件被触发,可以独立处理点击逻辑:确保这些元素的点击事件只在它们自身内部处理,不会影响到其他元素。

    private VisualElement cardView, upgradeView, upgradePreview;
    
    cardView.RegisterCallback<ClickEvent>(e => e.StopPropagation());
    upgradeView.RegisterCallback<ClickEvent>(e => e.StopPropagation());
    upgradePreview.RegisterCallback<ClickEvent>(e => e.StopPropagation());

2. 让拖拽卡牌的效果更还原杀戮尖塔的样子。

  • 我仔细观察了杀戮尖塔中在拖拽卡牌到敌人身上和释放时的效果和麦扣老师教程中实现的效果还是有些差别,于是我就尝试修改了卡牌拖拽的效果。

    首先先看看杀戮尖塔战斗中的卡牌拖拽效果:
    杀戮尖塔中的卡牌拖拽效果

    1. 鼠标移动到卡牌时,会放大、旋转效果回正、卡牌上移,相邻的卡牌会向当前移动的卡牌反方向,移动一小段,然后将手上移动的卡牌能完整的展示出来。
    2. 拖拽非攻击牌时,可以拖拽着卡牌移动到任何位置,但在松手时,如果超过一定的y轴高度,就会释放,否则返回到手牌中,或者移动过程中按下鼠标右键也会回到手牌中。
    3. 拖拽攻击牌时,将卡牌移动到Y轴的一定高度时,卡牌会移动到手牌正中心,此时如果松开鼠标左键,仍然处于可释放状态,只有移动到敌人身上才会释放出来,或者按下鼠标右键把卡牌返回到手牌中。

    首先需要在 Card 类上继承PointerEnterHandler, IPointerExitHandler,来处理鼠标指针进入和离开卡牌的事件。

    /// <summary>
    /// 鼠标进入卡牌时触发
    /// </summary>
    /// <param name="eventData"></param>
    public void OnPointerEnter(PointerEventData eventData)
    {
        if (isAnimation) return; // 如果正在动画中,则不执行
    
        enterCardScaleEvent.RaisEvent(this, this);  // 触发进入卡牌的动画事件
    
        transform.DOScale(Vector3.one * 1.3f, 0.2f);                        // 将卡牌放大
        transform.DOMove(new Vector3(originalPosition.x, -4f, originalPosition.z), 0.2f);      // 将卡牌向上移动
        transform.rotation = Quaternion.identity;                           // 将卡牌旋转回初始状态
        GetComponent<SortingGroup>().sortingOrder = 20;
    }
    
    /// <summary>
    /// 鼠标离开卡牌时触发
    /// </summary>
    /// <param name="eventData"></param>
    public void OnPointerExit(PointerEventData eventData)
    {
        if (isAnimation) return; // 如果正在动画中,则不执行
    
        exitCardScaleEvent.RaisEvent(this, this); // 触发离开卡牌的动画事件
        
        ResetCardTransform();
    }
    
    /// <summary>
    /// 重置卡牌位置和旋转
    /// </summary>
    public void ResetCardTransform()
    {
        transform.DOScale(Vector3.one, 0.2f);
        transform.SetPositionAndRotation(originalPosition , originalRotation);
        GetComponent<SortingGroup>().sortingOrder = originalLayerOrder;
    }

    其中在进入和移出的事件函数里还分别触发呼叫了enterCardScaleEventexitCardScaleEvent,这两个事件分别绑定在CardDeck.cs上面,用于处理卡牌进入时,将这张相邻的卡牌往当前移动进入的卡牌反方向移动一些距离。而处理卡牌离开时,需要执行一下CardLayoutReset(),将所有卡牌恢复到初始的位置和旋转,重新布局。

    
    /// <summary>
    /// 重置卡牌布局
    /// </summary>
    public void CardLayoutReset()
    {
        for (int i = 0; i < handCardList.Count; i++)
        {
            Card currentCard = handCardList[i];
            CardTransform cardTransform = cardLayoutManager.GetCardTransform(i, handCardList.Count);
    
            currentCard.transform.DOMove(cardTransform.pos, 0.2f).onComplete = () => currentCard.isAnimation = false;
        }
    }
    
    /// <summary>
    /// 检测鼠标放到手牌的某一张卡牌时触发的事件
    /// </summary>
    /// <param name="obj"></param>
    public void OnMouseEnterTorCard(object obj)
    {
        var card = (Card)obj;
    
        for (int i = 0; i < handCardList.Count; i++)
        {
            Card currentCard = handCardList[i];
            CardTransform cardTransform = cardLayoutManager.GetCardTransform(i, handCardList.Count);
    
            if (handCardList[i].cardIndex == card.cardIndex)
            {
                // card.isEventAnimation = true;
            }
            else
            {
                currentCard.transform.DOMoveX(handCardList[i].cardIndex < card.cardIndex ? cardTransform.pos.x - 0.6f : cardTransform.pos.x + 1f, 0.2f);
            }
        }
    }
    
    /// <summary>
    /// 检测鼠标从手牌的某一张卡牌移开时触发的事件
    /// </summary>
    /// <param name="obj"></param>
    public void OnMouseExitTorCard(object obj)
    {
        CardLayoutReset();
    }

    展示出来的效果就是这样了
    自己修改后卡牌拖拽的效果

今天暂时就写到这里了,后面有时间还会继续发文章到博客这里。


版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 时间永远嫌少的小白 !
  目录