第一章 动态加载人物
在根据chatgpt的指引下,我终于完成了从数据文件里调用模型加入我的BattleGo。当然一些脚本也是参考了以前的前辈(Unity通过协程实现回合制战斗 | Unity 中文课堂),然后我在上面添砖加瓦,缝缝补补。但他的这个教程是没有完成的,但我目前已经给这个教程添加了动画实现,还有人物动态加载(通过id)。
1.第一步:创建角色脚本
首先我是创建了一个脚本。使用unity自带的ScriptTable来创建角色,代码如下:
using UnityEngine;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
[CreateAssetMenu(fileName = "CharacterName", menuName = "创建战斗角色1", order = 1)]
public class CharacterData : ScriptableObject
{
public int id;
/// <summary>
/// 角色名
/// </summary>
public string characterName;
/// <summary>
/// 角色血量
/// </summary>
public int hp;
/// <summary>
/// 角色蓝量
/// </summary>
public int mp;
/// <summary>
/// 每回合回蓝量
/// </summary>
public int mpRecoverAmount;
/// <summary>
/// 角色攻击力
/// </summary>
public int atk;
/// <summary>
/// 暴击伤害(百分比)
/// </summary>
public int critDmg = 100;
/// <summary>
/// 暴击率
/// </summary>
[Range(0, 100)] public int critRate;
/// <summary>
/// 角色百分比防御力
/// </summary>
[Range(0, 100)] public int defRate;
/// <summary>
/// 角色百分比闪避率
/// </summary>
[Range(0, 100)] public int duckRate;
/// <summary>
/// 技能
/// </summary>
public Skill[] skills;
/// <summary>
/// 动画控制器路径
/// </summary>
public string aniContPath;
/// <summary>
/// 角色战斗物体加载
/// </summary>
public GameObject gameObjectBattle;
/// <summary>
/// 角色战斗物体加载路径
/// </summary>
public string gameObjectBattlePath;
public Character ToCharacter()
{
return new Character()
{
Name = characterName,
Hp = hp,
InitialHp = hp,
Mp = mp,
InitialMp = mp,
MpRecoverAmount = mpRecoverAmount,
Atk = atk,
CritDmg = critDmg,
CritRate = critRate,
DefRate = defRate,
DuckRate = duckRate,
Skills = skills,
AniContPath = aniContPath,
GameObjectBattle = gameObjectBattle,
GameObjectBattlePath = gameObjectBattlePath
};
}
}
public class Character
{
/// <summary>
/// 角色名
/// </summary>
public string Name;
/// <summary>
/// 角色初始血量
/// </summary>
public int InitialHp;
/// <summary>
/// 角色血量
/// </summary>
public int Hp;
/// <summary>
/// 角色初始蓝量
/// </summary>
public int InitialMp;
/// <summary>
/// 角色蓝量
/// </summary>
public int Mp;
/// <summary>
/// 每回合回蓝量
/// </summary>
public int MpRecoverAmount;
/// <summary>
/// 角色攻击力
/// </summary>
public int Atk;
/// <summary>
/// 暴击伤害(百分比)
/// </summary>
public int CritDmg;
/// <summary>
/// 暴击率
/// </summary>
public int CritRate;
/// <summary>
/// 角色百分比防御力
/// </summary>
public int DefRate;
/// <summary>
/// 角色百分比闪避率
/// </summary>
public int DuckRate;
/// <summary>
/// 角色是否死亡
/// </summary>
public bool Dead => Hp <= 0;
/// <summary>
/// 技能
/// </summary>
public Skill[] Skills;
/// <summary>
/// 动画控制器路径
/// </summary>
public string AniContPath;
/// <summary>
/// 技能cd字典
/// </summary>
public Dictionary<Skill, int> SkillCountdown = new Dictionary<Skill, int>();
/// <summary>
/// 动画控制器
/// </summary>
public AnimationManager AnimationManager { get; set; }
/// <summary>
/// 角色战斗物体加载
/// </summary>
public GameObject GameObjectBattle;
/// <summary>
/// 角色战斗物体加载路径
/// </summary>
public string GameObjectBattlePath;
}
有了这个脚本,我就能在Resources文件夹中创建我需要的角色表格了。(记得一定得是Resources,但凡多一个少一个字母都不行。方便后面使用Resources.Load<>()方法来进行加载人物表),大概就是这样:
2.第二步:建立角色数组
接着,我又创了个建立战斗人物表格数组的脚本:(本来想把这两个放一个脚本,结果发现会导致重启unity后,数组这个会丢失脚本。)
using UnityEngine;
[CreateAssetMenu(fileName = "CharacterDataTable", menuName = "创建战斗角色数组", order = 2)]
/// <summary>
/// 存储所有角色的数据表数组
/// </summary>
public class CharacterDataTable : ScriptableObject
{
public CharacterData[] characterDataArray;
}
有了他,我们就能去创建数组来容纳,上面所创的人物表格了。大概样子是这样滴。
大概样子是这样滴。这个战斗角色数组建立后,还可以建立背包数组、道具数组等等各种,最重要的是可以根据不同任务建立不同的角色数组,去进行任务人物的加载。比如说我想要这个A任务有4、5、6人物去作为敌人,那么直接将数组A设定为[4,5,6]就好了,然后加载数组A。
3.第三步:根据id获取角色数据
去GameManager里面新写一个方法,根据id去获取这个人物。
/// <summary>
/// 根据id获取角色战斗数组中的角色数据///
/// </summary>
/// <param name="characterId"></param>
/// <returns></returns>
public CharacterDataTable characterDataTable; // 指定ScriptableObject的表
public CharacterData GetCharacterDataById(int characterId)
{
//characterDataTable = new List<CharacterDataTable>(Resources.LoadAll<CharacterDataTable>("CharacterDatas/战斗角色数组"));
// 加载资源
characterDataTable = Resources.Load<CharacterDataTable>("CharacterDatas/BattleCharacterArray");
// 确保资源成功加载
if (characterDataTable == null)
{
Debug.LogError("没找到战斗数组");
return null;
}
foreach (var characterData in characterDataTable.characterDataArray)
{
if (characterData.id == characterId)
{
return characterData;
}
}
return null; // 如果找不到则返回null
}
记得路径得和自己的一样。
4.第四步:给角色赋值
然后去FightMgr(战斗逻辑脚本),给Players和Enemys赋值。
public CharacterDataTable characterDataTable; // 指定ScriptableObject的表
/// <summary>
/// 根据id加载角色
/// </summary>
/// <param name="characterId"></param>
public CharacterData LoadCharacterById(int characterId)
{
// 查找指定id的角色数据
CharacterData characterData = GameManager.Instance.GetCharacterDataById(characterId);
return characterData;
}
private IEnumerator Fight(Action sucCallback, Action lostCallback)
{
int round = 1;//回合数
bool playerTurn = true;//是否是玩家的回合
for (int i = 0; i < playerData.Length; i++)
{
playerData[i] = LoadCharacterById(i);
}
for (int i = 0; i < enemyData.Length; i++)
{
enemyData[i] = LoadCharacterById(i+3);
}
//这里用到了Select语法,传了一个lambda匿名委托,
//大概意思就是从playerData和enemyData数组中取每个元素的ToCharacter方法返回的对象,并转为新数组
Character[] player = playerData.Select(d => d.ToCharacter()).ToArray();
Character[] enemy = enemyData.Select(d => d.ToCharacter()).ToArray();
省略了
}
这里后面可以去设置任务id赋值。目前是写了Players的队伍是0.1.2,然后Enemy的队伍是3.4.5
然后后面自己运行了游戏,发现可以加载成功。心情很不错 ,哈哈哈。
4.1遇到问题:
目前的问题是我发现游戏物体可以赋值,但是游戏ui的赋值好像是一步到位。
运行后就直接变成这样,后面经过一番苦战,问题研究出来了。原来是我为了想优化代码运行逻辑,想在FightMgr脚本里直接存储一个游戏内敌人和友军物体的数组,并把他放在Awake()里(之所以这么放,是因为好让PlayerUIMgr脚本去引用它)所以才出现了这个问题。
我的解决办法:
先写了个方法
/// <summary>
/// 玩家队伍和敌人队伍的游戏物体引用
/// </summary>
private void InitGameObj()
{
for (int i = 0; i < gameObjectsP.Length; i++)
{
gameObjectsP[i] = GameObject.Find("Player/P" + (i + 1)+"(Clone)");
}
for (int i = 0; i < gameObjectsE.Length; i++)
{
gameObjectsE[i] = GameObject.Find("Enemy/E" + (i + 1)+"(Clone)");
}
}
①用这个方法去寻找加载的预制体物体,并把这个方法放在Fight协程开始的上方
②修改PlayerUIMgr脚本中的方法,给他再加一个参数playersObjects,方便从FightMgr的参数可以直接实现,就不需要在PlayerUIMgr脚本中再赋值一遍了。只需要让InitGameObj()方法在协程里进行赋值就OK了。
/// <summary>
/// 更新队友血量
/// </summary>
/// <param name="players"></param>
public void UpdatePlayerHp(Character[] players,GameObject[] playersObjects)
{
//for (int i = 0; i < gameObjectsP.Length; i++)
//{
// gameObjectsP[i] = GameObject.Find("Player/P" + (i + 1));
//}
for (int i = 0; i < players.Length; i++)
{
if (i < playersObjects.Length)
{
GameObject gameObject = playersObjects[i];
Transform hp = gameObject.transform.Find("Canvas/HP");
Transform hpText = gameObject.transform.Find("Canvas/HPText");
Slider hpSlider = hp.GetComponent<Slider>();
Text text = hpText.GetComponent<Text>();
hpSlider.maxValue = players[i].InitialHp;
hpSlider.value = players[i].Hp;
text.text = $"HP: {players[i].Hp}/{players[i].InitialHp}";
Transform animator = gameObject.transform.Find("ani");
Animator ani = animator.GetComponent<Animator>();
//ani.SetBool("IsDead", players[i].Dead);
}
}
}
OK 吃饭去了。
5.第五步:LoadPrefb()
这中间我又做了很多事,因为一些事,没有记下来。不过此篇笔记也只是给自己一个学习的记忆。
我又在GameManager里创了两个int数组,让他们用来存放玩家队伍和敌人队伍,战斗的时候只需要调用这个int整数就能获得id,然后再获得游戏物体。这确实是一个很便捷的办法。
关于Fight脚本的Start()里我就写了一个方法:LoadPrefb();具体代码如下:
/// <summary>
/// 加入角色表里的预制体
/// </summary>
private void LoadPrefb()
{
GameObject battleGo = GameObject.Find("BattleGo");
// 获取Player和Enemy物体
GameObject player = battleGo.transform.Find("Player").gameObject;
GameObject enemy = battleGo.transform.Find("Enemy").gameObject;
// 将预制体实例化到P1, P2, P3以及E1, E2, E3
for (int i = 0; i < playerData.Length; i++)
{
//加载角色战斗数据(因为我会把每个数组到时候初始化为-1,如果是-1说明该数组没有加载角色进来)
if (GameManager.Instance.playersIdArray[i] != -1)
{
playerData[i] = GameManager.Instance.LoadCharacterById(GameManager.Instance.playersIdArray[i]);
// 替换Player下的P1, P2, P3
GameObject t = GameManager.Instance.LoadGameObjectById(GameManager.Instance.playersIdArray[i]);
t.name = "P"+(i+1);
ReplaceWithPrefab(player.transform.Find("P" + (i + 1)), t);
gameObjectsP[i] = GameObject.Find("Player/P" + (i + 1) + "(Clone)");
//绑定好物体和数据,为后面技能随机生成的数组好赋值
playerData[i].gameObjId = i;
Debug.Log(GameManager.Instance.playersIdArray[i]);
}
else
{
continue;
}
}
for (int i = 0; i < enemyData.Length; i++)
{
//加载角色战斗数据(因为我会把每个数组到时候初始化为-1,如果是-1说明该数组没有加载角色进来)
if (GameManager.Instance.enemyIdArray[i] != -1)
{
enemyData[i] = GameManager.Instance.LoadCharacterById(GameManager.Instance.enemyIdArray[i]);
// 替换Enemy下的E1, E2, E3
GameObject t = GameManager.Instance.LoadGameObjectById(GameManager.Instance.enemyIdArray[i]);
t.name = "E" + (i + 1);
//Debug.Log(t.name);
ReplaceWithPrefab(enemy.transform.Find("E" + (i + 1)), t);
gameObjectsE[i] = GameObject.Find("Enemy/E" + (i + 1) + "(Clone)");
//绑定好物体和数据,为后面技能随机生成的数组好赋值
enemyData[i].gameObjId = i;
Debug.Log(GameManager.Instance.enemyIdArray[i]);
}
else
{
continue;
}
//gameObjectsE[i+3] = t;
}
void ReplaceWithPrefab(Transform targetTransform,GameObject gameObjectPrefab)
{
if (targetTransform != null)
{
// 获取原始物体
GameObject originalObject = targetTransform.gameObject;
// 记录原始物体的位置和父物体
Vector3 position = originalObject.transform.position;
Transform parent = originalObject.transform.parent;
// 删除原物体
Destroy(originalObject);
// 实例化新的预制体
Instantiate(gameObjectPrefab, position, Quaternion.identity, parent);
}
else
{
Debug.LogWarning("Target transform not found!");
}
}
OK,那么这样我就能很好的去更新血条和游戏物体了
因为我脚本里的技能释放目标是通过Random()随机生成的,所以opponent和gameObject并不能绑定到一起。当时就发现动画和血条物体等不匹配。我当时弄了一个小时左右吧(萌新的水平T_T),去问gpt,gpt叫我做个词典去映射,太扯了。我都是随机生成的,难道弄个动态映射。(可能是自己表达不清吧)在下去吃饭的时候突然想到,之前通过修改t.name来强制让他们变成P1、E1等,我是不是也可以在CharacterData里再添加一个属性GameObjId来锁定这个物体,在物体赋值时便进行修改。果不其然,这样的属性修改,比词典要好用的多。