Unity3D RPG实现 4

目录

添加血条

玩家升级

玩家UI信息

传送门

跨场景的切换

保存数据

制作封面

开始游戏时的动画

 场景转换的渐入渐出


扫描二维码关注公众号,回复: 13717725 查看本文章

添加血条

添加一个画布,可以看到默认的方式,以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防止再次产生。

猜你喜欢

转载自blog.csdn.net/weixin_43757333/article/details/123099014