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事件接口(IBeginDragHandler、IDragHandler、IEndDragHandler)来实现拖拽功能。
二、玩家选择行动的交互实现(结合拖拽玩法)
参考炉石的交互逻辑,我推荐的流程是:玩家拖拽卡牌到游戏区域→松开鼠标弹出行动选择面板→玩家选择行动后进入目标选择模式→点击目标触发对应效果。这样既符合玩家的拖拽习惯,又能明确区分双行动的选择。
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




