卡牌相关的方法的完成
卡牌相关的方法的完成
制作卡牌Prefab
Sorting Group
将 SortingGroup 组件添加到游戏对象将确保游戏对象后代中的所有渲染器都将一起排序和渲染。
这样可以将子物体合成一个整体来判断它的叠层关系,令不同的叠层有一个整体化的渲染效果
拥有 SortingGroup 的一个常见用例是创建由多个 SpriteRenderer组成的复杂 2D 角色。当这种角色的几个克隆重叠时,它们的各个身体部位可能无法正确排序,从而导致身体部位交错的视觉故障。例如,两个角色的手可能被排列在他们的身体前面,你会期望一个完整的角色被画在另一个角色的前面。SortingGroup 组件通过确保字符的整个分支一起排序和呈现来解决此问题。
SortingGroup 的后代使用相同的 SortingLayer 和 Renderer.sortingOrder 进行排序。但是,它们仅针对 SortingGroup 的其他后代进行排序,而不针对 SortingGroup 之外的任何渲染器进行排序。这允许您重用相同的 SortingLayers(例如,“手”、“躯干”等)对身体部位进行排序,同时确保它们永远不会与角色的其他克隆交错。
[Sorting Group](file:///D:/Unity.b/2021.3.21f1c1/Editor/Data/Documentation/en/ScriptReference/Rendering.SortingGroup.html)
Physics2D.Raycast
将Game Object当作UI组件,去判断与角色互动有关的事件
将指定层的内容像检测UI一样检测鼠标射线的碰撞关系
[Physics2D.Raycast](file:///D:/Unity.b/2021.3.21f1c1/Editor/Data/Documentation/en/ScriptReference/Physics2D.Raycast.html)
创建卡牌数据类
创建卡牌数据类
public class CardDataSO : ScriptableObject
{
public string cardName;
public Sprite cardImage;
public int cost;
public CardType cardType;
[TextArea]
public string description;
//TODO:实际的执行效果
}
实现将创建好的数据传入卡牌类中初始化卡牌样式
public class Card : MonoBehaviour
{
[Header("组件")]
public SpriteRenderer cardSprite;
public TextMeshPro costText, descriptionText, typeText,nameText;
public CardDataSO cardData;
private void Start()
{
Init(cardData);
}
public void Init(CardDataSO data)
{
cardData = data;
nameText.text = data.cardName;
cardSprite.sprite = data.cardImage;
costText.text=data.cost.ToString();
descriptionText.text=data.description;
typeText.text = data.cardType switch
{
CardType.Attack =>"Attack",
CardType.Defense => "Skill",
CardType.Abilities => "Abilities",
_ => throw new ArgumentOutOfRangeException()
};
}
}
对象池
创建对象池,并在初始化时预先生成卡牌
ObjectPool
基于堆栈的 IObjectPool。
对象池是一种优化项目并减轻 CPU 在必须快速创建和销毁新对象时的负担的方法。这是一个很好的做法和设计模式,可以帮助减轻 CPU 的处理能力,以处理更重要的任务,而不会被重复的创建和销毁调用所淹没。 ObjectPool 使用堆栈来保存对象实例的集合以供重用,并且不是线程安全的。
创建Card Manger
在资源列表中增添卡牌的数据,并在Card Manger之中进行资源加载的管理
//获取项目卡牌
private void InitializeCardDataList()
{//在文件夹中加载所有的资源
Addressables.LoadAssetsAsync<CardDataSO>("CardData",null).Completed += OnCardDataLoaded;
}
//回调函数
//回调方法来确定一下卡牌都已经获得了之后将它添加到列表当中
private void OnCardDataLoaded(AsyncOperationHandle<IList<CardDataSO>> handle)
{//项目初始化时可以收集到所有的卡牌数据类型
if (handle.Status == AsyncOperationStatus.Succeeded)
{
cardDataList = new List<CardDataSO>(handle.Result);
}
else
{
Debug.LogError("No CardDataSO found");
}
}
public GameObject GetCardObject()
{
return poolTool.GetObjectFromPool();
}//CardManger中返回的是对象池中的卡牌预制体
public void DiscardCard(GameObject cardObj)
{
poolTool.ReturnObjectToPool(cardObj);
}
CardManger中返回的是对象池中的卡牌预制体
制作卡牌库实现抽卡
卡牌库:卡牌数据+每种卡牌的张数
public class CardLibrarySO : ScriptableObject
{
public List<CardLibraryEntry> cardLibraryList;
}
[System.Serializable]
public struct CardLibraryEntry
{
public CardDataSO cardData;
public int count;
}
创建Card Deck实现抽牌方法
创建抽牌堆,弃牌堆,每回合的手牌列表
进行初始化,循环当前牌库,从卡牌库中获取到每张卡牌的数据并存入抽牌堆中
实现抽牌方法
抽几张牌进行几个循环
传出抽牌堆最开始的牌的数据并赋值给当前牌,将最开始牌的数据移除
从card Manger卡池中抽出一张牌并将刚刚得到的当前牌的数据进行这张牌的初始化
最后将牌添加到当前手牌的列表中
public class CardManger : MonoBehaviour
{
public PoolTool poolTool;
public List<CardDataSO> cardDataList;//游戏中所有可能出现的卡牌
[Header("卡牌库")]
public CardLibrarySO newGameCardLibrary;//初始卡牌库
public CardLibrarySO currentCardLibrary;//当前玩家牌库
public void Awake()
{
InitializeCardDataList();
foreach (var item in newGameCardLibrary.cardLibraryList)
{
currentCardLibrary.cardLibraryList.Add(item);
}
}
//获取项目卡牌
private void InitializeCardDataList()
{//在文件夹中加载所有的资源
Addressables.LoadAssetsAsync<CardDataSO>("CardData",null).Completed += OnCardDataLoaded;
}
//回调函数
//回调方法来确定一下卡牌都已经获得了之后将它添加到列表当中
private void OnCardDataLoaded(AsyncOperationHandle<IList<CardDataSO>> handle)
{//项目初始化时可以收集到所有的卡牌数据类型
if (handle.Status == AsyncOperationStatus.Succeeded)
{
cardDataList = new List<CardDataSO>(handle.Result);
}
else
{
Debug.LogError("No CardDataSO found");
}
}
public GameObject GetCardObject()
{
return poolTool.GetObjectFromPool();
}//返回的是对象池中的卡牌预制体
public void DiscardCard(GameObject cardObj)
{
poolTool.ReturnObjectToPool(cardObj);
}
}
创建卡牌布局
抽出的卡呈横向排列
创建Card Layout Manger管理抽卡时的卡牌布局
获取到每张牌的数量计算牌与牌之间的间距,并将卡牌的位置和旋转角度记录在数组中
private void CalculatePosition(int numberOfCards, bool horizontal)//计算牌间距和每张牌的位置
{
cardPosition.Clear();
if (horizontal)
{
float currentWidth = cardSpacing * (numberOfCards - 1);
float totalWidth = Mathf.Min(currentWidth, maxWidth);
float currentSpacing = totalWidth > 0 ? totalWidth / (numberOfCards - 1) : 0;//抽出一张以上的牌则计算宽度
for (int i = 0; i < numberOfCards; i++)
{
float xPos = 0 - (totalWidth / 2) + (i * currentSpacing);
var pos = new Vector3(xPos, centerPoint.y, 0f);
var rotation = Quaternion.identity;
cardPosition.Add(pos);
cardRotations.Add(rotation);
}
}
}
创建Card Transform记录每张卡牌的位置和旋转角度
public struct CardTransform
{
public Vector3 pos;
public Quaternion rotation;
public CardTransform(Vector3 vector, Quaternion quaternion)
{
pos = vector;
rotation = quaternion;
}
}
在抽牌脚本中获取每一张牌的角度和位置,并将当前卡牌设置到这个位置
private void SetCardLayout()//获取每一张卡牌的角度和位置
{
for(int i = 0; i < handCardObjectList.Count; i++)
{
Card currentCard = handCardObjectList[i];
CardTransform cardTransform = layoutManager.GetCardTransform(i,handCardObjectList.Count);
currentCard.transform.SetPositionAndRotation(cardTransform.pos,cardTransform.rotation);
}
}
抽出的卡呈扇形排列
设计函数将传入的角度转换为坐标
private Vector3 FanCardPosition(float angle)
{//将角度转换为坐标
return new Vector3(
(float)(centerPoint.x-Math.Sin(Mathf.Deg2Rad*angle)*radius),
(float)(centerPoint.y+Math.Cos(Mathf.Deg2Rad*angle)*radius),
0
);
}
利用Math.Deg2Rad可以将角度变成弧度
float cardAngle = (numberOfCards - 1) * angleBetweenCards / 2;
for (int i = 0; i < numberOfCards; i++)
{
var pos = FanCardPosition(cardAngle - i * angleBetweenCards);
var rotation=Quaternion.Euler(0, 0, cardAngle - i * angleBetweenCards);
cardPosition.Add(pos);
cardRotations.Add(rotation);
}
抽卡动画
解决无限抽卡的问题,在每次程序停止后对卡牌库进行清空
private void OnDisable()
{//解决无限抽卡的问题,在每次程序停止后对卡牌库进行清空
currentCardLibrary.cardLibraryList.Clear();
}
利用DoTweening中的方法调整抽出卡时卡牌的尺寸和位置角度,实现动画效果
利用SetDelay方法设置一个延迟,让多张牌抽出时实现一张一张调整位置的效果,而不是一起移动
实现鼠标事件
设计函数存储卡牌的原始位置和角度
public void UpdatePositionRotation(Vector3 position, Quaternion rotation)
{
originalPosition = position;
originalRotation = rotation;
originalLayOrder = GetComponent<SortingGroup>().sortingOrder;
}
添加IPointerEnterHandler,IPointerExitHandler接口,实现鼠标划入时卡牌位置上升并显示在所有卡牌最高层,鼠标离开时回归原来的位置
这里根据自己的喜好做了一些小改动,将原教程卡牌位置直接变动改为了用DOTWEEN实现的一些动画效果
public void OnPointerEnter(PointerEventData eventData)
{//鼠标划入时使其位置上升,并显示在所有卡牌最高层
if (isAnimating)
{
return;
}
transform.DOMove(originalPosition + Vector3.up, 0.3f);
transform.DORotateQuaternion(Quaternion.identity, 0.3f);
/*transform.position = originalPosition + Vector3.up;*/
/*transform.rotation = Quaternion.identity;*/
GetComponent<SortingGroup>().sortingOrder = 20;
}
public void OnPointerExit(PointerEventData eventData)
{
if (isAnimating)
{
return;
}
transform.DOMove(originalPosition , 0.3f);
transform.DORotateQuaternion(originalRotation, 0.3f);
/*transform.position = originalPosition;*/
/*transform.rotation = originalRotation;*/
GetComponent<SortingGroup>().sortingOrder = originalLayOrder;
}
以及在这里原教程选择了创建一个新函数RestCardTransform恢复位置,我直接在这个函数里改变量值了,如果日后遇到bug可以考虑是这里的问题
public void RestCardTransform()
{
transform.SetPositionAndRotation(originalPosition,originalRotation);
GetComponent<SortingGroup>().sortingOrder = originalLayOrder;
}
卡牌拖拽
创建卡牌拖拽脚本。添加三个接口MonoBehaviour,IBeginDragHandler,IDragHandler,IEndDragHandler
在IBeginDragHandler中根据卡牌类型判断是否课拖拽
public void OnBeginDrag(PointerEventData eventData)
{
switch (currentCard.cardData.cardType)
{
case CardType.Abilities:
canMove = true;
break;
case CardType.Attack:
break;
case CardType.Defense:
break;
}
}
在IDragHandler中进行屏幕坐标和世界坐标的转换,实现拖拽效果
public void OnDrag(PointerEventData eventData)
{
if (canMove)
{
currentCard.isAnimating = true;
Vector3 screenPos = new(Input.mousePosition.x, Input.mousePosition.y, 10);
Vector3 wordPos = Camera.main.ScreenToWorldPoint(screenPos);//将屏幕位置转换为世界位置
currentCard.transform.position= wordPos;
canExcute = wordPos.y > 1f;
}
}
在IEndDragHandler中实现卡牌在没有拖到指定位置时返回原位的功能
currentCard.transform.position = currentCard.originalPosition;
currentCard.transform.rotation = currentCard.originalRotation;
currentCard.isAnimating = false;
攻击牌的拖拽指针
创建指针材质
绘制贝塞尔曲线,根据几个点控制这个曲线的样子
public class DragArrow : MonoBehaviour
{
private LineRenderer lineRenderer;
private Vector3 mousePos;
public int pointsCount;
public float arcModifier;
void Awake()
{
lineRenderer = GetComponent<LineRenderer>();
}
private void Update()
{
mousePos=Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 10.0f));
SetArrowPosition();
}
public void SetArrowPosition()
{
Vector3 cardPosition = transform.position; // 卡牌位置
Vector3 direction = mousePos - cardPosition; // 从卡牌指向鼠标的方向
Vector3 normalizedDirection = direction.normalized; // 归一化方向
// 计算垂直于卡牌到鼠标方向的向量
Vector3 perpendicular = new(-normalizedDirection.y, normalizedDirection.x, normalizedDirection.z);
// 设置控制点的偏移量
Vector3 offset = perpendicular * arcModifier; // 你可以调整这个值来改变曲线的形状
Vector3 controlPoint = (cardPosition + mousePos) / 2 + offset; // 控制点
lineRenderer.positionCount = pointsCount; // 设置 LineRenderer 的点的数量
for (int i = 0; i < pointsCount; i++)
{
float t = i / (float)(pointsCount - 1);
Vector3 point = CalculateQuadraticBezierPoint(t, cardPosition, controlPoint, mousePos);
lineRenderer.SetPosition(i, point);
}
}
//计算贝塞尔曲线
Vector3 CalculateQuadraticBezierPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2)
{
float u = 1 - t;
float tt = t * t;
float uu = u * u;
Vector3 p = uu * p0; // 第一项
p += 2 * u * t * p1; // 第二项
p += tt * p2; // 第三项
return p;
}
}
实现洗牌逻辑
洗牌分为两种情况
从抽牌堆中抽取
private void ShuffleDeck()//洗牌
{
discardDeck.Clear();
for (int i = 0; i < drawDeck.Count; i++)
{
CardDataSO temp = drawDeck[i];
int randowIndex = UnityEngine.Random.Range(i, drawDeck.Count);
drawDeck[i]=drawDeck[randowIndex];
drawDeck[randowIndex]=temp;
}
}
从弃牌堆中抽取
if (drawDeck.Count == 0)
{
//TODO:洗牌/从弃牌堆中抽取
foreach (var item in discardDeck)
{
drawDeck.Add(item);
}
ShuffleDeck();
}
添加事件广播监听抽牌堆和弃牌堆数量UI更改的事件
遇到的bug
currentCard.transform.DOScale(Vector3.one, 0.2f).SetDelay(delay).onComplete=()=>
{//确保在放大之后才执行移动
currentCard.transform.DOMove(cardTransform.pos, 0.5f);
currentCard.transform.DORotateQuaternion(cardTransform.rotation, 0.5f);
};
LoadAssetAsync和LoadAssetsAsync生成的回调函数分别是AsyncOperationHandle和AsyncOperationHandle<IList>
这俩函数特别像,对比了半天都没看出来,还是意外改对的
使卡牌呈扇形排列
刚开始效果还挺好,但是不知道为什么后来排列的有些乱七八糟的
原因:没有清空存储卡牌角度的列表,导致在下一次运行时列表中还有数据,于是导致了画面的不美观