You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

Unity双行动卡牌开发问询:编码实现与玩家行动选择逻辑

嘿,这个双行动卡牌的需求我之前做类似TCG项目的时候刚好碰过,结合炉石那种拖拽交互的思路,给你拆解下Unity里的实现步骤,分核心逻辑和交互处理两部分来讲,都是实战里能用的方案~

一、先理清楚双行动卡牌的核心架构

首先你已经有了Action1和Action2的独立参数,那第一步要把卡牌的数据和行为分离,用ScriptableObject来存卡牌的配置(比如伤害值、是否有回血行动),这样后续改数值或者加新卡牌都不用动逻辑代码。

1. 卡牌数据类(ScriptableObject)

这个类用来存储卡牌的双行动参数,方便在Unity编辑器里直接编辑:

using UnityEngine;

[CreateAssetMenu(fileName = "NewDualActionCard", menuName = "Cards/Dual Action Card")]
public class DualActionCardData : ScriptableObject
{
    // 行动1:对敌方造成的伤害值
    public int damageAmount = 2;
    // 行动2:标记是否支持满血恢复
    public bool canHealToFull = true;
}

2. 卡牌行为脚本(挂在卡牌Prefab上)

这个脚本负责处理拖拽、触发行动选择、执行效果的核心逻辑,我们用Unity的UI事件接口(IBeginDragHandlerIDragHandlerIEndDragHandler)来实现拖拽功能。

二、玩家选择行动的交互实现(结合拖拽玩法)

参考炉石的交互逻辑,我推荐的流程是:玩家拖拽卡牌到游戏区域→松开鼠标弹出行动选择面板→玩家选择行动后进入目标选择模式→点击目标触发对应效果。这样既符合玩家的拖拽习惯,又能明确区分双行动的选择。

1. 卡牌拖拽+行动选择触发逻辑

先写卡牌的拖拽和弹出选择面板的代码:

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class DualActionCardBehaviour : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    [SerializeField] private DualActionCardData cardData;
    [SerializeField] private GameObject actionSelectionPanel; // 提前做好的双行动选择面板(两个按钮)
    
    private RectTransform _cardRect;
    private CanvasGroup _canvasGroup;
    private Vector2 _originalPos;
    private Button _damageBtn;
    private Button _healBtn;

    void Awake()
    {
        _cardRect = GetComponent<RectTransform>();
        _canvasGroup = GetComponent<CanvasGroup>();
        _originalPos = _cardRect.anchoredPosition;

        // 初始化选择面板的按钮事件
        if (actionSelectionPanel != null)
        {
            _damageBtn = actionSelectionPanel.transform.Find("DamageButton").GetComponent<Button>();
            _healBtn = actionSelectionPanel.transform.Find("HealButton").GetComponent<Button>();
            
            _damageBtn.onClick.AddListener(OnDamageActionChosen);
            _healBtn.onClick.AddListener(OnHealActionChosen);
            actionSelectionPanel.SetActive(false);
        }
    }

    // 开始拖拽时的处理
    public void OnBeginDrag(PointerEventData eventData)
    {
        _canvasGroup.alpha = 0.7f; // 半透明效果
        _canvasGroup.blocksRaycasts = false; // 拖拽时不阻挡其他UI点击
        transform.SetParent(transform.root); // 把卡牌移到最上层,避免被遮挡
    }

    // 拖拽中的位置更新
    public void OnDrag(PointerEventData eventData)
    {
        _cardRect.anchoredPosition += eventData.delta / GetComponentInParent<Canvas>().scaleFactor;
    }

    // 结束拖拽时的逻辑
    public void OnEndDrag(PointerEventData eventData)
    {
        _canvasGroup.alpha = 1f;
        _canvasGroup.blocksRaycasts = true;
        transform.SetParent(transform.parent.parent); // 放回原父物体

        // 判断是否拖拽到游戏区域(这里需要你提前定义一个游戏区域的RectTransform)
        Rect gameAreaRect = GameObject.Find("GamePlayArea").GetComponent<RectTransform>().rect;
        Vector2 localPoint;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            GameObject.Find("GamePlayArea").GetComponent<RectTransform>(),
            eventData.position,
            eventData.pressEventCamera,
            out localPoint);

        if (gameAreaRect.Contains(localPoint))
        {
            // 弹出选择面板,放在鼠标位置附近
            actionSelectionPanel.SetActive(true);
            actionSelectionPanel.GetComponent<RectTransform>().position = eventData.position;
        }
        else
        {
            // 拖拽到区域外,放回原位
            _cardRect.anchoredPosition = _originalPos;
        }
    }

    // 选择伤害行动后的逻辑
    private void OnDamageActionChosen()
    {
        actionSelectionPanel.SetActive(false);
        // 通知目标选择管理器,进入敌方目标选择模式
        TargetSelector.Instance.StartEnemyTargetSelection((targetShip) =>
        {
            targetShip.TakeDamage(cardData.damageAmount);
            // 处理卡牌状态(比如移出手牌、进入弃牌堆,根据你的规则来)
            Destroy(gameObject);
        });
    }

    // 选择回血行动后的逻辑
    private void OnHealActionChosen()
    {
        actionSelectionPanel.SetActive(false);
        // 通知目标选择管理器,进入我方目标选择模式
        TargetSelector.Instance.StartAllyTargetSelection((targetShip) =>
        {
            targetShip.HealToFull();
            // 处理卡牌状态
            Destroy(gameObject);
        });
    }
}

2. 目标选择管理器(统一处理目标选择)

这个单例类用来管理目标选择的状态,避免每个卡牌都写重复的选择逻辑:

using UnityEngine;
using System;
using System.Collections.Generic;

public class TargetSelector : MonoBehaviour
{
    public static TargetSelector Instance;

    private Action<Ship> _onTargetConfirmed;
    private List<Ship> _selectableShips = new List<Ship>();

    void Awake()
    {
        if (Instance == null) Instance = this;
        else Destroy(gameObject);
    }

    // 开启敌方目标选择
    public void StartEnemyTargetSelection(Action<Ship> callback)
    {
        _onTargetConfirmed = callback;
        _selectableShips.Clear();
        
        foreach (Ship ship in FindObjectsOfType<Ship>())
        {
            if (ship.IsEnemy)
            {
                _selectableShips.Add(ship);
                ship.EnableSelection(true); // 开启高亮和点击检测
            }
        }
    }

    // 开启我方目标选择
    public void StartAllyTargetSelection(Action<Ship> callback)
    {
        _onTargetConfirmed = callback;
        _selectableShips.Clear();
        
        foreach (Ship ship in FindObjectsOfType<Ship>())
        {
            if (!ship.IsEnemy)
            {
                _selectableShips.Add(ship);
                ship.EnableSelection(true);
            }
        }
    }

    // 当玩家点击飞船时调用
    public void OnShipSelected(Ship selectedShip)
    {
        if (_selectableShips.Contains(selectedShip))
        {
            _onTargetConfirmed?.Invoke(selectedShip);
            // 关闭所有飞船的选择状态
            foreach (Ship ship in _selectableShips)
            {
                ship.EnableSelection(false);
            }
            _selectableShips.Clear();
        }
    }
}

3. 飞船(目标)的逻辑脚本

这个脚本处理飞船的伤害、回血,以及选择状态的反馈:

using UnityEngine;
using UnityEngine.EventSystems;

public class Ship : MonoBehaviour, IPointerClickHandler
{
    public bool IsEnemy;
    public int MaxHealth;
    private int _currentHealth;

    private SpriteRenderer _spriteRenderer;
    private Color _originalColor;

    void Awake()
    {
        _spriteRenderer = GetComponent<SpriteRenderer>();
        _originalColor = _spriteRenderer.color;
        _currentHealth = MaxHealth;
    }

    // 受到伤害
    public void TakeDamage(int damage)
    {
        _currentHealth -= damage;
        if (_currentHealth < 0) _currentHealth = 0;
        Debug.Log($"{gameObject.name} 受到 {damage} 伤害,当前血量: {_currentHealth}");
        // 这里可以加血量UI的更新逻辑
    }

    // 恢复至满血
    public void HealToFull()
    {
        _currentHealth = MaxHealth;
        Debug.Log($"{gameObject.name} 已恢复至满血!");
        // 更新血量UI
    }

    // 开启/关闭选择状态(高亮+可点击)
    public void EnableSelection(bool enable)
    {
        _spriteRenderer.color = enable ? Color.yellow : _originalColor;
        GetComponent<Collider2D>().enabled = enable;
    }

    // 点击飞船时触发
    public void OnPointerClick(PointerEventData eventData)
    {
        TargetSelector.Instance.OnShipSelected(this);
    }
}
三、一些实战优化建议
  • 选择面板适配:弹出行动选择面板时,要判断面板是否超出屏幕边界,自动调整位置,避免玩家看不到按钮。
  • 拖拽层级:拖拽卡牌时,把它的父物体设为Canvas的根节点,确保卡牌在最上层,不会被其他UI挡住。
  • 移动端适配:如果要支持手机,建议用Unity的Input System来统一处理鼠标和触摸事件,替换掉PointerEventData的相关逻辑。
  • 反馈强化:目标选择时,除了高亮,还可以加鼠标悬停的提示文字,或者点击时的动画效果,提升玩家体验。

内容的提问来源于stack exchange,提问作者Indy-Jones

火山引擎 最新活动