(一)设计玩家升级系统
在属性代码CharacterData_SO中添加代表经验值系统的变量,对于不同类型的对象,比如敌人没有升级系统而玩家有,则不需要的变量不对其赋值即可。设计好等级、经验值、等级增幅和击败敌人获得的经验值,并在造成伤害的方法中判断死亡并提供经验值(由于逻辑是承担伤害所以可以这样调用)。修改后的数据代码:
public class CharacterData_SO : ScriptableObject
{
[Header("Stats Info")]
public int maxHealth;
public int currentHealth;
public int maxDefence;
public int currentDefence;
[Header("kill")]
public int killPoint;
[Header("Level")]
public int currentLevel;
public int maxLevel;
public int baseExp;//升级所需经验值
public int currentExp;
public float levelBuff;//每次升级属性的提升幅度
public float LevelMultiplier//每级具体的提升
{
get
{
return 1 + (currentLevel - 1) * levelBuff;
}
}
public void UpdateExp(int point)//用于将杀敌获得的point加入到currentExp中
{
currentExp += point;
if (currentExp >= baseExp)
{
LevelUp();
}
}
private void LevelUp()
{
currentLevel=Mathf.Clamp(currentLevel+1, 0,maxLevel);//将等级提升限制在范围内
baseExp += (int)(baseExp * LevelMultiplier);//每一级所需经验值增加
maxHealth=(int)(maxHealth*LevelMultiplier);
currentHealth = maxHealth;
//Debug.Log("Level up:" + currentLevel + "\tMax Health:" + maxHealth);//test
}
}
(二)制作玩家生命值UI
就像敌人血条UI一样,玩家生命值UI也从创建Canvas开始,其RenderMode设置为Screen Space来保证UI显示在屏幕平面上(毕竟不需要像敌人的一样跟随)。在创建的Image中设置锚点位置:在此界面按住alt+shift去选择即可保证无论屏幕如何缩放都可保持image距离边缘的距离。同样在Canvas中创建代表经验值的条和文本,将Canvas的UI Scale Mode改为根据屏幕尺寸调整,从而保证在不同大小分辨率下能够保持相同的效果。
编写的控制脚本需要挂载在对应的Canvas上。由于player的UI需要在各个场景中都有显示,所以要将UI保存为预制体,并在脚本中获得Canvas的所有子对象。获取对象后,在Update方法中同步数据与UI。之前制作怪物血条时,由于敌人血条UI脚本是挂载到敌人对象上的,因此可以通过.GetComponent<>()来获得数据,而玩家在创建时在GameManager中注册过,且GameManager是一个单例调用方便,所以可以从GameManager中获得数据。
完整的playerUI:
public class PlayerHealthUI : MonoBehaviour
{
Text levelText;//等级文本
Image healthSlider;
Image expSlider;
private void Awake()
{
//获取子物体
levelText = transform.GetChild(2).GetComponent<Text>();//子物体按顺序从0开始编号
healthSlider=transform.GetChild(0).GetChild(0).GetComponent<Image>();
expSlider= transform.GetChild(1).GetChild(0).GetComponent<Image>();
}
private void Update()
{
levelText.text = "Level: "+GameManager.Instance.playerStats.characterData.currentLevel.ToString("00");//使用ToString可以改变字符串转化方法与样式
UpdateHealth();
UpdateExp();
}
void UpdateHealth()//更新血条方法
{
float sliderPercent = (float)((float)GameManager.Instance.playerStats.CurrentHealth / (float)GameManager.Instance.playerStats.MaxHealth);
healthSlider.fillAmount = sliderPercent;
}
void UpdateExp()//更新经验值方法
{
float expPercent = (float)((float)GameManager.Instance.playerStats.characterData.currentExp / (float)GameManager.Instance.playerStats.characterData.baseExp);
expSlider.fillAmount = expPercent;
}
}
(三)制作传送门
使用shader graph创建一个传送门。先在projects中创建一个lit shader graph,通过为shader graph添加节点来不断改变其图形效果。传送门的动态效果首先需要旋转,将twirl节点和voronoi节点相连接形成效果更好的漩涡状。
节点的参数中,UV是与xyz对应的参数,其匹配关系为:(图片引自教程视频)
在游戏中,传送门会不断地旋转,因此还要添加时间节点,并添加速度参数,与Time相乘将结果连接到旋转上来控制旋转动画。旋转的强度也需要另设变量来控制。结果输出前需要先对图像的形状等进行处理。创建Sample Texture2D节点和相关变量并配置初始值,将结果与旋转结果相乘,能够得到成圆形的旋转图案,再创建color变量来控制图案的颜色。最终结果连接到主节点Emission(自发光)上,并设置透明通道。如果将结果连接到主节点的Alpha上后发现预览图中没有结果,则可以尝试改为将上一层Multiply结果连到Alpha上。shadergraph结果:
使用做好的shader创建材质,挂载到quad对象上,调整相关参数并在MeshRender中取消其阴影。在shader中调整颜色模式为HDR并修改亮度(注意shader需要手动保存)。效果仍然不满意的话,也可以在shader中继续添加参数进行修改,如使用power节点增强强度。
创建其他传送门时,要先基于shader生成新的材质,再在新材质上修改。保存传送门预制体时需要将子物体图标设为none,否则会报错。
下面设计传送门的传送功能,让玩家在传送门处按e能够转移位置到传送门的目的地。创建传送脚本和传送目的地脚本。每个传送门由门和目的地两部分组成,传送的逻辑则是搜索同场景或目标场景下与门的目的地Tag相同的目的地,并在那里将player生成。
将门的碰撞体的isTrigger打开以用于判断是否碰撞。当Player在碰撞范围内时将传送门表示允许传送的bool变量打开,反之关闭。
在进行下一步之前,利用已有素材创建新的场景。后面实现不同场景传送时将采用异步加载的形式。
(四)实现同场景传送
创建SceneController脚本来专门管理场景转换(注意SceneManager为unity自带类型,取名时要注意)。在TransitionPoint中检测键盘是否按下e键且为可传送状态,在Update中实现此功能,而传送的具体实现则在SceneController单例中进行。用SceneManager.LoadSceneAsync来对场景进行异步加载,并使用AsyncOperation来获得加载场景的进度,具体使用方法见此接口的官方文档。获取到玩家对象后,使用Transform的Set型方法来设置坐标实现传送的移动效果。
由于传送门的碰撞体挡住了射线,无法移动到传送门处,因此需要在MouseManager处修改来增加到传送门的点击寻路。由于传送不会使NavMeshAgent重置,因此在传送方法实现中也需要使agent停止。
(五)实现跨场景传送
在协程中异步加载传送到的场景,并使用yield return等待异步加载完成后再进行后面的传送。出于游戏设计,每次转换场景时应当把原本的玩家角色生成出来而非在每个场景都创建一个玩家,所以在加载完成后创建角色。完成后跳出协程,后续工作中会补充在传送前保存当前状态的功能。场景需要在buildsettings中进行设置才能被识别到。
在切换场景后,原本场景的管理器脚本就会随对象一起消失,因此需要在各个Manager的Awake中加入。而此时也需要在PlayerController中将原本订阅的事件取消,即需要将玩家对事件的订阅和取消订阅放在玩家生成和销毁时执行,这样就可以防止传送后玩家无法控制player。但是切换场景经历了player的销毁和构建,此时摄像机跟随会失效,由于GameManager会第一时间获得生成出的player信息,所以可以在GameManager中,在player注册后查找场景中是否有对应的CinemaChine摄像机,如果存在则将其follow对象重设为player。
在测试时发现如下错误:“NullReferenceException: Object reference not set to an instance of an object PlayerController.OnEnable () (at Assets/Script/characters/PlayerController.cs:39)“,此句原功能为将方法登记到事件中,查看运行过程发现是在MouseManager加载之前PlayerController.OnEnable ()就已经运行,根据评论方法,可以通过在project settings 的Execution Order中设置执行顺序来解决。
TransitionPoint传送门脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TransitionPoint : MonoBehaviour
{
public enum TransitionType//是同场景传送还是不同场景
{
SameScene,DifferentScene
}
[Header("Transition Info")]
public string sceneName;//传送到的场景名
public TransitionType transitionType;
public TransitionDestination.DestinationTag destinationTag;//传送目的地的类型,入口或其他
private bool canTrans;//判断能否传送
private void Update()
{
if (Input.GetKeyDown(KeyCode.E) && canTrans)
{
//调用SceneController的传送方法。
SceneController.Instance.TransitionToDestination(this);
}
}
private void OnTriggerStay(Collider other)//isTrigget的碰撞体碰撞期间执行
{
if(other.CompareTag("Player"))//使传送门成为可传送状态
{
canTrans = true;
}
}
private void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player"))
{
canTrans = false;
}
}
}
TransitionDestination传送目的地类:文章来源:https://www.toymoban.com/news/detail-752641.html
public class TransitionDestination : MonoBehaviour
{
public enum DestinationTag
{
Enter,A,B,C,Z
}
public DestinationTag destinationTag;//传送点类型
}
SceneController场景管理脚本:文章来源地址https://www.toymoban.com/news/detail-752641.html
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;
//设置为单例模式,本质为场景管理Manager
public class SceneController : Singleton<SceneController>
{
public GameObject PlayerPrefab;//用于跨场景时生成玩家
GameObject player;//被传送的对象
NavMeshAgent playerAgent;
protected override void Awake()
{
base.Awake();
DontDestroyOnLoad(this);//防止Manager被销毁而无法继续执行功能
}
public void TransitionToDestination(TransitionPoint transitionPoint)
{
switch (transitionPoint.transitionType)
{
case TransitionPoint.TransitionType.SameScene://同场景传送
StartCoroutine(Transition(SceneManager.GetActiveScene().name, transitionPoint.destinationTag));//以当前场景名和目的地作为参数
break;
case TransitionPoint.TransitionType.DifferentScene://不同场景传送
StartCoroutine(Transition(transitionPoint.sceneName, transitionPoint.destinationTag)); //以目标场景和目的地作为参数
break;
}
}
IEnumerator Transition(string sceneName , TransitionDestination.DestinationTag destinationTag)//进行传送的方法。
{
if (SceneManager.GetActiveScene().name == sceneName)//同场景传送
{
//Debug.Log("SceneName:" + sceneName);//test
player = GameManager.Instance.playerStats.gameObject;//先获得玩家的游戏对象
//再将玩家位置设置为终点位置
playerAgent = player.GetComponent<NavMeshAgent>();
playerAgent.enabled = false;//暂停Agent防止玩家自动导航回传送门处
player.transform.SetPositionAndRotation(GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);
playerAgent.enabled = true;
yield return null;
}
else//跨场景传送
{
//TODO: Save data
yield return SceneManager.LoadSceneAsync(sceneName);//异步加载场景
yield return Instantiate(PlayerPrefab, GetDestination(destinationTag).transform.position,GetDestination(destinationTag).transform.rotation);
yield break;//跳出协程
}
}
private TransitionDestination GetDestination(TransitionDestination.DestinationTag destinationTag)//搜索符合标签的传送终点并返回。
{
var entrances = FindObjectsOfType<TransitionDestination>();//找到所有可作为终点的对象
for(int i = 0; i < entrances.Length; i++)
{
if (entrances[i].destinationTag== destinationTag)
{
return entrances[i];
}
}
return null;
}
}
到了这里,关于3D RPG Course | Core | Unity学习笔记(八)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!