目录
添加血条
添加一个画布,可以看到默认的方式,以2d形式查看是覆盖整个面的。
我们想要用世界坐标的,修改:然后
reset position。
创建文件夹添加包:
将包中的拖进来
然后将这个square放进ui里,然后再创建一个子图片,并修改
然后将其作为预制体
创建一个代码并在此将prefab拖入:(而不是挂接到物体身上再拖入)
创建UI的代码
并在project中为其赋值好
为所有敌人的预制体添加该脚本并调整血条位置,然后创建空物体用作指代血条位置:
接下来我们需要在take damage处修改UI:
此处使用action实现,每个action的事件触发的时候,能够激活所有订阅它的函数。
在此处创建一个事件:
尖括号里的int代表订阅它的函数必须有两个int型的参数。
上面实现了启用事件的方法,下面写事件具体执行的方法:修改血条以及添加订阅。
但是此处还没有生成,接下来写生成:
(初始时血条不可见,攻击时才可见)
(选中函数按下f2键可以统一修改选中的函数。)
然后为每个怪物添加UI脚本,并将位置赋值
这样子即可实现血条:
但是血条不会跟着人走,因此此处修改。
让血条的位置跟着玩家身上的位置走,但是朝向朝向玩家的反向。
接下来希望实现过一段时间,血条消失,用一个计时器
记得在脚本里添加这个更新使得血条的位置可以跟随玩家移动,视角也可以转换
private void LateUpdate()
{
if (UIbar != null)
{
UIbar.position = barPoint.position;
UIbar.forward = -cam.forward;
}
}
完整代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class HealthBarUI : MonoBehaviour
{
public GameObject healthUIPrefab;
public Transform barPoint;//固定在怪物上方的UI的位置
Image healthSlider;
Transform UIbar;//当前UI位置
Transform cam;//用于使当前UI朝向对应的方向
CharacterStats currentStats;
public bool alwaysVisible;//该参数实现是否可见
public float visibleTime;
void Awake()
{
currentStats = GetComponent<CharacterStats>();
currentStats.UpdateHealthBarOnAttack += UpdateHealthBar;
}
void OnEnable()
{
//实现对所有的
cam = Camera.main.transform;
foreach(Canvas canvas in FindObjectsOfType<Canvas>())
{
if (canvas.renderMode == RenderMode.WorldSpace)
{
Debug.Log("Canvas in World ");
UIbar = Instantiate(healthUIPrefab, canvas.transform).transform;
healthSlider = UIbar.GetChild(0).GetComponent<Image>();//获取遮盖住的绿色滑动条
UIbar.gameObject.SetActive(alwaysVisible);
}
}
}
private void UpdateHealthBar(int currentHealth, int maxHealth)
{
if (currentHealth <= 0) Destroy(UIbar.gameObject);
UIbar.gameObject.SetActive(true);
float sliderPercent = (float)currentHealth / maxHealth;
healthSlider.fillAmount = sliderPercent;
}
private void LateUpdate()
{
if (UIbar != null)
{
UIbar.position = barPoint.position;
UIbar.forward = -cam.forward;
}
}
}
玩家升级
在characterStats中添加代码:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName ="New Data",menuName ="Character Stats/Data")]
public class CharacterData_SO : ScriptableObject
{
[Header("Stats Info")]
public int maxHealth;
public int currentHealth;
public int baseDefence;
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)
{
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("Levelup" + currentLevel + " Max Health:" + maxHealth);
}
}
然后再设定数据:
玩家UI信息
传送门
加入time可以实现随时间旋转的效果:
然后用这个创建材质,创建Quad并放入
开了这个才能看到动画效果
关掉传送门的阴影
将参数color中的模式改成HDR
,即可在inspector窗口中修改亮度了
为了方便修改cell density,设定一个变量作为其输入:
这样可以使得强度更亮。
接下来实现传送门代码的实现:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TransitionDestination : MonoBehaviour
{
public enum DestinationTag
{
ENTER,A,B,C
}
public DestinationTag destinationTag;
}
这个设定传送门的一些信息
下面这个设定逻辑:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TransitionPoint : MonoBehaviour
{
public enum TransitionType
{
SameScene,DifferentScene
}
private bool canTrans;
[Header("Transition Info")]
public string sceneName;
public TransitionType transitionType;
public TransitionDestination.DestinationTag destinationTag;
void OnTriggerStay(Collider other)
{
if (other.CompareTag("Player"))
canTrans = true;
}
private void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player"))
canTrans = false;
}
void Update()
{
if (Input.GetKeyDown(KeyCode.E) && canTrans)
{
//TODO:
SceneController.Instance.TransitionToDestination(this);
}
}
}
再写一个场景内位置的转换:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneController : Singleton<SceneController>
{
GameObject player;
public void TransitionToDestination(TransitionPoint transitionPoint)
{
switch (transitionPoint.transitionType)
{
case TransitionPoint.TransitionType.SameScene:
StartCoroutine(Transition(SceneManager.GetActiveScene().name, transitionPoint.destinationTag));
break;
case TransitionPoint.TransitionType.DifferentScene:
break;
}
}
IEnumerator Transition(string sceneName,TransitionDestination.DestinationTag destinationTag)
{
player = GameManager.Instance.playerStats.gameObject;
player.transform.SetPositionAndRotation(GetDestionation(destinationTag).transform.position, GetDestionation(destinationTag).transform.rotation);
yield return null;
}
//此处以组件作为返回类型,是因为我们需要该组件的位置和朝向,而一个函数只能有一个返回值,此处通过获取该组件然后得到其位置和朝向
private TransitionDestination GetDestionation(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;
}
}
这个点设定的tag是A,代表传送的目标点是A
在这个门下面的子物体设定的就是该点是什么点:
(此处意思就是该点是入口,传送目标点是A点)
传送门的鼠标设定:
以及移动的位置调用:
为了使得位置的移动比较精确,可以修改传送门的trigger大小:
防止传送后人走回去,设定停止:
跨场景的切换
除此之外,还需要在传送门的inspetor中设定传送的目标点和名字。
为使得跨场景传送后仍然有玩家,需要将几个manager跨场景保留:(GameManager、MouseManager同理)
并且还要设定玩家的preafab。
为使得传送回来仍然可以有相机跟随:
效果如下:
保存数据
对于玩家来说,我们将其数据的存储方式改为和其他敌人一样,用template data赋值给character data的模式。
记得apply to prefab
使用PlayerPrefs来存储数据:
查阅手册可得它只能保留这三种类型的值:
这里只能保存string类型的,要怎么保存SO类型的呢?
使用JSON可以将SO序列化存储后导出成string的形式,
官方手册里:
先将其SO保存成json类型,然后再保存成string类型即可保存在磁盘中。
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SaveManager : Singleton<SaveManager>
{
protected override void Awake()
{
base.Awake();
DontDestroyOnLoad(this);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.S))
{
SavePlayerData();
}
if (Input.GetKeyDown(KeyCode.L))
{
LoadPlayerData();
}
}
public void SavePlayerData()
{
Save(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);
}
public void LoadPlayerData()
{
Load(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);
}
public void Save(Object data,string key)
{
var jsonData = JsonUtility.ToJson(data,true);
PlayerPrefs.SetString(key, jsonData);
PlayerPrefs.Save();
}
public void Load(Object data,string key)
{
if (PlayerPrefs.HasKey(key))
{
JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString(key), data);
}
}
}
保存后存储下的数据:
场景切换时自动保存数据,如果切换到不同场景则自动读取数据:
这样就可以实现切换场景仍然保留数据
制作封面
创建一个新的场景,然后设定画布
然后设定下名字和一个游戏的按钮:
之后为了做一些特效,此处就选择这个效果, 并将main camera放进去
这个时候的渲染模式就是把画布放在摄像机前面了一样,此时如下图所示,按钮就会被地面所挡住
此处可以实现一个点击开始游戏之后,摄像机前移而字后移的效果,那就是把画布先放到前面作为渲染相机的模式,然后再改为作为world space的模式。
接下来设定场景中的按钮:达到按下显色的效果
点击开始游戏后需要进行场景切换和生成,此处不能调用之前那个scene,因此我们新写一个协程:
还需要在初始场景设定一个传送门。
新的协程:
将读取入口的操作放在一直存在的GameManager:
新的协程:
这样即可实现new game。
接下来实现continue game:
存储时多一个把当前场景也存进去:
然后 将continuegame的函数在此实现
而加载数据,放在PlayerController中的Start一开始去实现:
此处对之前的一个地方做更改,将注册信息放在OnEnable中实现:
然后在MainMenu中添加场景的转换函数:
此处在添加一个回到初始界面的场景操作:
然后当按下Esc键时,调用该函数:
接下来就可以进行从开始界面进入标题界面,再从标题界面进入游戏界面了。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Playables;
public class MainMenu : MonoBehaviour
{
Button newGameBtn;
Button continueBtn;
Button quitBin;
PlayableDirector director;
private void Awake()
{
newGameBtn = transform.GetChild(1).GetComponent<Button>();
continueBtn = transform.GetChild(2).GetComponent<Button>();
quitBin = transform.GetChild(3).GetComponent<Button>();
newGameBtn.onClick.AddListener(PlayTimeLine);
continueBtn.onClick.AddListener(ContinueGame);
quitBin.onClick.AddListener(QuitGame);
director = FindObjectOfType<PlayableDirector>();
director.stopped += NewGame;
}
void PlayTimeLine()
{
director.Play();
}
void NewGame(PlayableDirector obj)
{
PlayerPrefs.DeleteAll();
//转换场景
SceneController.Instance.TransitionToFirstLevel();
}
void ContinueGame()
{
//转换场景,读取进度
SceneController.Instance.TransitionToLoadGame();
}
void QuitGame()
{
Debug.Log("退出游戏");
Application.Quit();
}
}
开始游戏时的动画
然后为相机添加时间轴,然后将camera拖拽进来,并录制:
选中camera,在position处右键添加key,然后录制相机移动。
将人物和移动动画拖拽进来:
并设置移动动画
添加override track并设定动画:
设定好时间线后在脚本里控制:
按下开始键时播放时间线
在时间线结束的事件添加开始游戏的订阅:(添加订阅需要参数匹配所以这里给newgame添加了参数)
为防止播放动画时,玩家可以按键,所以这时候用时间线操控eventSystem的不可操控性:
场景转换的渐入渐出
创建一个图片,按下shift并选中即可覆盖整个面。
然后虽然可以通过调整图片的alpha实现渐入渐出,此处实现其他的功能。
这个canvas group可以实现是否可以互动,是否遮挡射线。
渐入渐出就通过控制此处的alpha实现。
接下来怎么实现在游戏的运行过程中透明度从0变到1呢,这种一个事件伴随另一个事件发生通常使用协程:
public class SceneFader : MonoBehaviour
{
CanvasGroup canvasGroup;
public float fadeInDuration=2.5f;
public float fadeOutDuration=2.5f;
// Start is called before the first frame update
private void Awake()
{
canvasGroup = GetComponent<CanvasGroup>();
DontDestroyOnLoad(gameObject);//一直用所以不删掉
}
public IEnumerator FadeOutIn()
{
yield return FadeOut(fadeOutDuration);
yield return FadeIn(fadeInDuration);
}
public IEnumerator FadeOut(float time)
{
while (canvasGroup.alpha < 1)
{
canvasGroup.alpha += Time.deltaTime / time;
yield return null;
}
}
public IEnumerator FadeIn(float time)
{
while (canvasGroup.alpha !=0)
{
canvasGroup.alpha -= Time.deltaTime / time;
yield return null;
}
}
}
在场景控制器LoadLevel时调用SceneFader中的渐入渐出协程
然后给scene controller添加即可:
注意别忘了初始设定alpha为0:
此处补充一点,把指针修改一下,其他情况默认为鼠标状态:
玩家死亡时,我们希望场景控制器也做出一些动作(回到初始场景),所以此处实现调用观察者模式接口:
然后在需要实现的函数里添加加载主场景即可:
但是此处有一点需要注意,玩家死亡是一个状态,当玩家死亡时,可能会持续不断的执行该协程,因此这里设置一个变量判断是否播放过。
设定一个变量初始设为true,然后:
每次切换场景后销毁该SceneFader防止再次产生。