Spine
Spine是一个收费的跨平台的2D骨骼动画制作工具。
Spine动画的使用
玩家或敌人的血量有变化时都会执行对应的方法
人物基类代码
玩家或敌人的血量变化直接通过事件执行方法
IntVariable有一些通用的与int数据有关的值以及当数据被更改时启动的事件
创建与IntVariablele类相关联的,IntEventSO数据类,IntEventListener监听类,以及IntEventSOEditor编辑器相关的代码
public class IntVariable :ScriptableObject
{
public int maxValue;
public int currentValue;
public IntEventSo ValueChangedEvent;
[TextArea] [SerializeField]
private string description;
public void SetValue(int value)//设置数值,启动事件
{
currentValue = value;
ValueChangedEvent.RaisEvent(value,this);
}
}
创建CharacterBase人物基类,实现人物受伤扣血方法和开局满血方法
public class CharacterBase : MonoBehaviour
{
public int maxHp;
public IntVariable hp;
public int CurrentHp { get=>hp.currentValue; set=>hp.SetValue(value); }
public int MaxHp
{
get => hp.maxValue;
}
protected Animator animator;
private bool isDead;
public void Awake()
{
animator = GetComponent<Animator>();
}
protected void Start()//设置敌人一开始的血量为满血
{
hp.maxValue = maxHp;
CurrentHp = MaxHp;
}
public virtual void TakeDamage(int damage)//收到伤害的方法
{
if (CurrentHp > damage)
{
CurrentHp -= damage;
Debug.Log(CurrentHp);
}
else
{
CurrentHp = 0;
isDead = true;
}
}
}
执行卡牌效果
给卡牌信息添加效果列表存储该卡牌可执行的效果,比如对目标造成伤害
创建效果基类,包括数值,对敌方产生的效果类型,执行方法(谁来执行,目标是谁)
public abstract class Effect:ScriptableObject
{
public int value;
public EffectTargetType targetType;
public abstract void Execute(CharacterBase from, CharacterBase target);//抽象类只写声明不写实现,具体实现由子类完成
}
创建伤害效果类,根据效果类型执行不同的操作
public class DemageEffect : Effect
{
public override void Execute(CharacterBase from, CharacterBase target)
{
if (target == null)
{
return;
}
switch (targetType)
{
case EffectTargetType.Target:
target.TakeDamage(value);
Debug.Log($"执行了{value}点伤害");
break;
case EffectTargetType.All:
foreach (var enemy in GameObject.FindGameObjectsWithTag("Enemy"))
{
enemy.GetComponent<CharacterBase>().TakeDamage(value);
}
break;
}
}
}
实现攻击卡牌拖动到怪物身上后执行的效果
else
{
if (eventData.pointerEnter == null)
{
return;
}
if (eventData.pointerEnter.CompareTag("Enemy"))//检测到敌人后锁定敌人执行方法
{
canExcute = true;
targetCharacter = eventData.pointerEnter.GetComponent<CharacterBase>();
return;
}
//松开鼠标停止拖拽时将target清空
canExcute = false;
targetCharacter = null;
}
public void OnEndDrag(PointerEventData eventData)
{
if (currentArrow != null)
{
Destroy(currentArrow);
}
if (canExcute)//松开鼠标执行方法
{
currentCard.ExceCardEffects(currentCard.player,targetCharacter);
}
else
{
currentCard.RestCardTransform();
currentCard.isAnimating = false;
}
}
创建回收卡牌的方法,并在Card预制体中添加监听事件
//实现弃牌逻辑
public void DisableCard(object obj)
{
Card card=obj as Card;
discardDeck.Add(card.cardData);
handCardObjectList.Remove(card);
cardManger.DiscardCard(card.gameObject);//调用Manger中的回收方法
SetCardLayout(0);
}
Unity中使用GameObject.Find()、FindWithTag()、FindGameObjectsWithTag()等函数的方法
制作血条的UI Document
这集主要是拼UI,以后熟练一下UI Toolkit的使用方法,感觉还是挺好用的
UI Toolkit
血条控制脚本
调用UIToolkit中的组件实现血条在玩家和敌人头上显示
public class HealthBarController : MonoBehaviour
{
[Header("Elements")]
public Transform healthBarTransform;
private UIDocument healthBarDocument;
private ProgressBar healthBar;
private void Awake()
{
healthBarDocument = GetComponent<UIDocument>();
healthBar = healthBarDocument.rootVisualElement.Q<ProgressBar>("HealthBar");//获得根节点使用名字的方式去查找该组件
MoveToWorldPosition(healthBar,healthBarTransform.position,Vector2.zero);
}
private void MoveToWorldPosition(VisualElement element, Vector3 worldPosition,Vector2 size)
{//UI组件的transform是Rect transform,所以要将屏幕尺寸转换为世界坐标
Rect rect = RuntimePanelUtils.CameraTransformWorldToPanelRect(element.panel, worldPosition, size, Camera.main);
element.transform.position = rect.position;
}
[ContextMenu("Get UI Position")]
private void Test()
{
healthBarDocument = GetComponent<UIDocument>();
healthBar = healthBarDocument.rootVisualElement.Q<ProgressBar>("HealthBar");//获得根节点使用名字的方式去查找该组件
MoveToWorldPosition(healthBar,healthBarTransform.position,Vector2.zero);
}
}
绑定血条数据
在HealthBarController中创建人物基类的对象
用对象中的血量最大值限制血条的数据
通过调用healthBar中的title,value修改血条上的文字和血条的长度,display设置血条的可见性
通过以上所说的方式更新血条状态
public void UpdataHealthBar()
{
if (currentCharacter.isDead)
{
healthBar.style.display = DisplayStyle.None;
return;
}
if (healthBar != null)
{
healthBar.title= $"{currentCharacter.CurrentHp}/{currentCharacter.maxHp}";
healthBar.value = currentCharacter.CurrentHp;
}
}
创建USS血条样式
使用 USS 时,可为内置的 VisualElement 属性或 UI 代码中的自定义属性指定值。
除了从 USS 文件中读取值之外,还可以使用 C#(通过 C# 的 VisualElement 属性)指定内置属性值。使用 C# 指定的值将覆盖 Unity 样式表 (USS) 中的值。
可使用自定义属性来扩展 USS。自定义 USS 属性需要 – 前缀。
Ease动画曲线参考
通过USS文件修改血条的大小数据和不同状态下的背景颜色
.unity-progress-bar__container {
height: 40px;
min-height: 40px;
}
.unity-progress-bar__progress {
height: 32px;
border-radius: 7px;
margin: 2px;
background-color: rgb(57, 197, 187);
transition: right 0.5s ease-out-circ,background-color 0.5s ease-in-out;
}
.unity-progress-bar__background {
margin: 0;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 10px;
border-color: black;
border-width: 2px;
}
.highHealth .unity-progress-bar__progress{
background-color: rgb(57, 197, 187);
}
.mediumHealth .unity-progress-bar__progress {
background-color: rgb(255,165,0);
}
.lowHealth .unity-progress-bar__progress {
background-color: rgb(216,0,0);
}
在HealthBarController中添加根据血量百分比调整血条颜色的方法
if (percentage < 0.3f)
{
healthBar.AddToClassList("lowHealth");
}else if (percentage < 0.6f)
{
healthBar.AddToClassList("mediumHealth");
}
else
{
healthBar.AddToClassList("highHealth");
}
}
制作Gameplay Panel
添加能量条抽牌堆弃牌堆
创建结束回合的UI组件,利用伪类实现鼠标移入UI图标变大的效果
目前有个问题,UI在所有物品上方挡住了卡牌,导致卡牌无法拖拽
TODO:给回合的UI组件添加回合转换的动态效果
绑定Gameplay Panel数据
public IntEventSo drawCountEvent;
public IntEventSo discardCountEvent;
添加事件,在牌堆的数字发生改变时调用事件同步到UI中,实现UI面板的数字同步
drawCountEvent.RaisEvent(drawDeck.Count, this);
回合转换
为回合切换创建脚本
创建玩家回合开始,敌人回合开始,敌人回合结束的事件,并创建不同的事件函数对其进行监听
public class TurnBaseManger : MonoBehaviour
{
private bool isPlayerTurn = false;
private bool isEnemyTurn = false;
public bool battleEnd = true;
private float timeCounter;
public float enemyTurnDuration;//敌人回合等待时间
public float playerTurnDuration;//玩家回合等待时间
[Header("事件广播")]
public ObjectEventSO playerTurnBegin;
public ObjectEventSO enemyTurnBegin;
public ObjectEventSO enemyTurnEnd;
/// <summary>
/// 在update里时刻检测回合的状态并进行切换
/// </summary>
private void Update()
{
if (battleEnd)
{
return;
}
if (isEnemyTurn)
{
timeCounter += Time.deltaTime;
if (timeCounter >= enemyTurnDuration)
{
timeCounter = 0f;
//敌人回合结束
//玩家回合开始
isPlayerTurn = true;
}
}
if (isPlayerTurn)
{
timeCounter += Time.deltaTime;
if (timeCounter >= playerTurnDuration)
{
timeCounter = 0;
//玩家回合开始
PlayerTurnBegin();
isPlayerTurn = false;
}
}
}
/// <summary>
/// 在每回合开始时重置回合状态
/// </summary>
[ContextMenu("Game Start")]
public void GameStart()
{
isPlayerTurn = true;
isEnemyTurn = false;
battleEnd = false;
timeCounter = 0;
}
/// <summary>
/// 创建不同回合的事件函数
/// </summary>
public void PlayerTurnBegin()
{
playerTurnBegin.RaisEvent(null,this);
}
public void EnemyTurnBegin()
{
isEnemyTurn = true;
}
public void EnemyTurnEnd()
{
isEnemyTurn = true;
}
}
在Gameplay Panel中添加玩家回合结束事件,并通过按钮对事件进行调用
public ObjectEventSO playerTurnEndEvent;//玩家回合结束
private void OnEndTurnButtonClick()
{
playerTurnEndEvent.RaisEvent(null,this);
}
实现玩家回合结束时清空牌堆的逻辑
public void OnPlayerTurnEnd()
{
//清空手牌
for (int i = 0; i < handCardObjectList.Count; i++)
{
discardDeck.Add(handCardObjectList[i].cardData);
cardManger.DiscardCard(handCardObjectList[i].gameObject);
}
handCardObjectList.Clear();
discardCountEvent.RaisEvent(discardDeck.Count,this);
}
出牌能量判断
分别在敌方回合和玩家回合使用不同的标题,并在敌方回合时禁用回合转换按钮
public void OnEnemyTurnBegin()
{
endTurnButton.SetEnabled(false);
turnLable.text="敌方回合";
turnLable.style.color = new StyleColor(Color.red);
}
public void OnPlayerTurnBegin()
{
endTurnButton.SetEnabled(true);
turnLable.text = "玩家回合";
turnLable.style.color = new StyleColor(Color.white);
}
在player中添加在每回合开始时初始化能量,更新能量的方法
public IntVariable playerMana;
public int maxMana;
public int CurrentMana
{
get => playerMana.currentValue;
set => playerMana.SetValue(value);
}
private void OnEnable()
{
playerMana.maxValue = maxMana;
CurrentMana = playerMana.maxValue;//设置初始法力值
}
/// <summary>
/// 监听事件函数
/// </summary>
public void NewTurn()
{
CurrentMana = maxMana;
}
public void UpdateMana(int cost)
{
CurrentMana -= cost;
if (CurrentMana <= 0)
{
CurrentMana = 0;
}
}
在卡牌的实现效果的方法中实现减少能量的事件方法
costEvent.RaisEvent(cardData.cost,this);
因为在每次抽牌打牌时都需要调用SetCardLayout布局,所以可以将出牌时的能量判断写在其中
将判断玩家当前能量是否足够打出该牌和修改卡牌能量字体的方法写到卡牌类中并进行调用
public void UpdateCardState()
{
isAvailiable = cardData.cost <= player.CurrentMana;
costText.color = isAvailiable ? Color.green : Color.red;
}
在卡牌拖拽的类中添加对卡牌当前状态的判断,如果能量不足则无法拖拽
防御牌及UI
在人物基类中创建防御值的更新和重置,并更改在有防御值的条件下所受到的伤害
public virtual void TakeDamage(int damage)//收到伤害的方法
{
var currentDamage = (damage - defense.currentValue) >= 0 ? (damage - defense.currentValue) : 0;
var currentDefense = (damage - defense.currentValue) >= 0 ? 0: (defense.currentValue - damage) ;
defense.SetValue(currentDefense);
if (CurrentHp > damage)
{
CurrentHp -= damage;
/*Debug.Log(CurrentHp);*/
}
else
{
CurrentHp = 0;
isDead = true;
}
}
public void UpdateDefense(int amount)
{
var value = defense.currentValue + amount;
defense.SetValue(value);
}
//每回合开始重置防御值
public void ResetDefense()
{
defense.SetValue(0);
}
基于Effect基类创建防御类
public override void Execute(CharacterBase from,CharacterBase target)
{
if (targetType==EffectTargetType.Self)
{
from.UpdateDefense(value);
}
if (targetType == EffectTargetType.Target)
{
target.UpdateDefense(value);
}
}
在Health bar下创建人物防御的UI,在HealthBarControl中添加控制防御UI的显示和数值变化
//防御属性
defenseElement.style.display = currentCharacter.defense.currentValue>0?DisplayStyle.Flex:DisplayStyle.None;
defenseAmountLabel.text = currentCharacter.defense.currentValue.ToString();
回血的苹果牌及特效
创建苹果卡牌的数据和添加加血效果的逻辑并为卡牌添加这个逻辑
public class HealEffect : Effect
{
public override void Execute(CharacterBase from, CharacterBase target)
{
if (targetType==EffectTargetType.Self)
{
from.HealHealth(value);
}
if (targetType == EffectTargetType.Target)
{
target.UpdateDefense(value);
}
}
}
在人物基类中添加加血方法
public void HealHealth(int amount)
{
CurrentHp += amount;
CurrentHp = Mathf.Min(CurrentHp, MaxHp);
buff.SetActive(true);
}
在玩家和怪物类下添加buff和debuff的特效物体,创建VFXController脚本进行特效的控制
public class VFXController : MonoBehaviour
{
public GameObject buff;
public GameObject debuff;
private float timeCounter;
private void Update()
{
if (buff.activeInHierarchy)
{
timeCounter += Time.deltaTime;
if (timeCounter >= 1.2f)
{
timeCounter = 0f;
buff.SetActive(false);
}
}
if (debuff.activeInHierarchy)
{
timeCounter += Time.deltaTime;
if (timeCounter >= 1.2f)
{
timeCounter = 0f;
debuff.SetActive(false);
}
}
}
}
增加力量牌及UI
创建力量牌的卡牌数据以及增伤效果
public class StrengthEffect :Effect
{
public override void Execute(CharacterBase from, CharacterBase target)
{
switch (targetType)
{
case EffectTargetType.Self:
from.SetupStrength(value,true);
break;
case EffectTargetType.Target:
target.SetupStrength(value,false);
break;
case EffectTargetType.All:
break;
}
}
}
在人物基类中添加受到buff和debuff的方法,并在每回合更新buff持续的回合数
public void SetupStrength(int round,bool isPositive)
{
if (isPositive)
{
float newStrength = baseStrength + strengthEffect;
baseStrength = Mathf.Min(newStrength, 1.5f);
buff.SetActive(true);
}
else
{
debuff.SetActive(true);
baseStrength = 1 - strengthEffect;
}
var currentRound = buffround.currentValue + round;
//敌人施加的debuff减到基础值时将debuff回合数清零
if (baseStrength == 1)
{
buffround.SetValue(0);
}
else
{
buffround.SetValue(currentRound);
}
}
/// <summary>
/// 回合转换事件函数
/// </summary>
public void UpdateStrengthRound()
{
buffround.SetValue(buffround.currentValue-1);
if (buffround.currentValue <= 0)
{
buffround.SetValue(0);
baseStrength = 1;
}
}
在HealthBarControl方法中添加buff和debuff的UI控制,实现每回合更新UI中的buff持续时间和受到不同的buff显示不同UI
//buff回合更新
buffElement.style.display=currentCharacter.buffround.currentValue>0?DisplayStyle.Flex:DisplayStyle.None;
buffRound.text = currentCharacter.buffround.currentValue.ToString();
buffElement.style.backgroundImage =
currentCharacter.baseStrength > 1 ? new StyleBackground(buffsprite) : new StyleBackground(debuffsprite);
增加抽卡牌和debuff牌
public class DrawCardEffect : Effect
{
public IntEventSo drawCardEvent;
public override void Execute(CharacterBase from, CharacterBase target)
{
//实现抽卡效果
drawCardEvent?.RaisEvent(value,this);
}
}
debuff牌的逻辑在上一节课的buff牌中已包括
遇到的Bug:
测试方法无法使用
理解不了,测试用方法实现不了,把UIDoucument删了重写了一下就行了
然后测试方法能用了,但是启动游戏的时候玩家的血条能显示,敌人的显示不出来了,可是明明都是用的同一个脚本同一个血条,为什么一个就行一个就不行啊妈的,神经病
骂完突然想到了一种可能,转换坐标时使用到了摄像机的位置,是不是因为敌人是创建在特殊场景中的,而场景中压根没有摄像机,所有才会显示不出来且报空
妈的我真是天才
教程中好像也讲了这个bug,但是不是我想的这个问题,似乎是代码的执行顺序问题
因为执行位置转换的代码写在Awake里,所以UIdoucument还未得到初始化就执行了该代码,只需要将该代码转移到start中执行就可以
在创建了Game Panel后,卡牌无法进行拖动
UI把鼠标射线挡住了,将UI中的Picking Mode调整为Ignore就可以将UI忽略了
从弃牌堆中抽取的卡牌无法实现正确的效果
在弃牌堆中抽取卡牌时因为弃牌已经被使用过,所以canExcute和canMove都被赋过值,应设置方法初始化这两个值,保证在使用前两变量没有初值
private void OnDisable()
{
canMove = false;
canExcute = false;
}
EventSO要写成Scriptobject类型,通过拖拽挂载在不同物体中给代码中赋值,从而进行调用
监听事件要通过组件挂载在物体上,并赋要监听的对象,response
在调用按钮事件时报空
找了最久的一个bug,结果是喜闻乐见的名字写错环节
在代码中进行初始化时,按钮是在根目录下通过名字查找并进行赋值,根目录下的名称和查找时输入的名称不一致时就会出现报空
回合开始后尽管卡牌能量足够但卡牌数字依旧显示为红色且卡牌无法打出
仅限于上一回合将能量用完的情况,如果上一回合能量没有用完,则卡牌颜色还是绿色
代码的执行顺序问题,抽卡代码和回合开始重置能量的代码调用了同一个事件,卡牌的能量判断在前,能量的恢复在后,所以出现了这种问题
如图将原先挂载在player身上的监听删除,在抽卡代码之前添加player的新回合更新能量的代码