第三部分

GameManager

单例模式

注意如果脚本命名为GameManager它会变成齿轮图标,虽然没什么用

创建GameManager类(后面还会贴这只是个骨架)

1
2
3
4
5
6
7
8
9
10
public class GameManager : Singleton<GameManager>
{

public CharacterStats playerStats;

public void RegisterPlayer(CharacterStats player)
{
playerStats = player;
}
}

创建工具Singleton泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Singleton<T> : MonoBehaviour where T:Singleton<T>
{
private static T instance;

public static T Instance
{
get { return instance; }
}

protected virtual void Awake()
{
if (instance != null)
{
Destroy(gameObject);
}
else
{
instance = (T)this;
}
}

public static bool isInitialized
{
get { return instance != null; }
}

protected virtual void OnDestoy()
{
if(instance == this)
{
instance = null;
}
}
}

之后把各种Manager类的父类从Monobehaviour改成Singleton即可

  • 泛型类无论在java还是C#中都可以使用

  • C#中的方法默认并不是virtual类型的,因此要添加virtual关键字才能够被重写,这点与java不同

  • where类似于数据库的那种用法起到约束作用

观察者模式进行广播

其实就是让所有敌人变成观察者,玩家被观察

增加接口IEndGameObserver

1
2
3
4
public interface IEndGameObserver
{
void EndNotify();
}

让EnemyController实现这个接口,并实现接口方法如下

1
2
3
4
5
6
7
8
9
10
11
12
public void EndNotify()
{
//获胜动画
//停止所有移动
//停止Agent
anim.SetBool("Win", true);
PlayerDead = true;
isChase = false;
isWalk = false;
attackTarget = null;

}

在EnemyController中增加函数

1
2
3
4
5
6
7
8
9
10
11
//  private void OnEnable()
//{
//注意这里加载
// GameManager.Instance.AddObserver(this);
//}
//OnDisable在OnDestory之后调用
private void OnDisable()
{
if (!GameManager.isInitialized) return;//注意这里由于这个会在游戏终止时调用一次,所以可能打包不会报错但是编辑器报错,所以在这添加一句话
GameManager.Instance.RemoveObserver(this);
}
  • 注意这里OnEnable被注释掉是因为如果正常写会出现异常,原因是不同物体上的脚本挂载顺序不同,可能这个脚本OnEnable调用的时候GameManager还没挂载

  • 有关于动画器图层的相关内容可以看这篇博客https://blog.csdn.net/qq_34122194/article/details/79029685

更多敌人

多个史莱姆

如果仅仅是复制粘贴史莱姆那么会出现Data共用的问题,应该创建一个模板数据templateData,然后增加Awake方法如下

1
2
3
4
5
6
7
private void Awake()
{
if(templateData != null)
{
characterData = Instantiate(templateData);
}
}

乌龟

没什么好说的…代码和方法大致和史莱姆一样

  • 如果想把一个动画器当做模板更方便地改变其中的动画,可以选择添加动画器覆盖控制器,更改原动画器则动画覆盖控制器也会随之修改

兽人

花了我很长时间…

创建代码Grunt类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Grunt : EnemyController
{
[Header("Skill")]
public float kickForce;

public void KickOff()
{
if (attackTarget != null)
{
transform.LookAt(attackTarget.transform);

Vector3 direction = attackTarget.transform.position - transform.position;//原理是向量相减指向被减向量
direction.Normalize();//相当于向量的单位化
//击飞
attackTarget.GetComponent<NavMeshAgent>().isStopped = true;
attackTarget.GetComponent<NavMeshAgent>().velocity = direction * kickForce;//NavMeshAgent自己就有速度向量
attackTarget.GetComponent<Animator>().SetTrigger("Dizzy");
}
}
}

创建代码状态机类(这里就不贴上了,创建这个状态机代码的目的是让玩家晕眩动画时不能进行移动

  • 代码状态机其实就是在动画上挂个脚本

有几个bug困扰了我好久

  • 明明代码里写了在攻击范围内会停止,但是史莱姆会还是会一直挤人

原因很简单,调整一下攻击范围大小

  • 兽人攻击后无法击退

这个解决方法是先把玩家的代理的自动刹车关掉,再把停止距离调到大致为(0.5到1)

(如果开启自动刹车再开启停止距离可能会出现停止后滑动一会的情况,这个见仁见智)

NavMeshAgent的具体用法可参考文档(点Unity组件上的?即可查看)

如何让敌人打空而不是出刀必中

这里使用扩展方法类ExtensionMethod,设置敌人攻击范围为一个扇形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public static class ExtensionMethod
{
private const float dotThreshold = 0.5f;
public static bool IsFacingTarget(this Transform transform,Transform target)
{
var vectorToTarget = target.position - transform.position;
vectorToTarget.Normalize();

float dot = Vector3.Dot(transform.forward, vectorToTarget);//这个就是两个向量点乘
return dot >= dotThreshold;
}
}

接下来在EnemyController里的Hit类里面直接调用 if (attackTarget != null && transform.IsFacingTarget(attackTarget.transform))即可

  • this后的参数类似于通知这个方法将会被哪个类调用,并非实参,而后面那个才是实参

石头人

终于要设计Boss了

设置石头人Boss

其实抛开扔石头跟设置兽人差不多,近战击退远程扔石头,动画器也复刻的兽人的,创建了个Golem脚本,等下写完扔石头会贴代码

把之前的问题修复一下

  • 暴击改为攻击者暴击

  • 因为石头人体积较大,所以停止距离应该更大才能到达石头人处停止,故想攻击时使人物停止距离和攻击范围一样大,而走路时回归原始的停止距离

  • 同样因为石头人体积大,石头人寻路的障碍躲避(相当于寻路的碰撞体)半径小一点就可以打到

  • 挂状态机代码使人物受伤时不能攻击(代码之前那个StopAgent类就可以)

可扔的石头

贴个代码Golem类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Golem : EnemyController
{
[Header("Skill")]
public float kickForce;

public GameObject rockPrefab;
public Transform handPos;
public void KickOff()
{
if (attackTarget != null && transform.IsFacingTarget(attackTarget.transform))
{
var targetStats = attackTarget.GetComponent<CharacterStats>();

Vector3 direction = attackTarget.transform.position - transform.position;
direction.Normalize();
targetStats.GetComponent<NavMeshAgent>().isStopped = true;
targetStats.GetComponent<NavMeshAgent>().velocity = direction * kickForce;
//个人喜好添加
targetStats.GetComponent<Animator>().SetTrigger("Dizzy");
targetStats.TakeDamage(characterStats, targetStats);
}
}

public void ThrowRock()
{
if(attackTarget != null)
{
var rock = Instantiate(rockPrefab, handPos.position, Quaternion.identity);
rock.GetComponent<Rock>().target = attackTarget;
}
}
}

Rock类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Rock : MonoBehaviour
{
private Rigidbody rb;
[Header("Basic Setting")]
public float force;
public GameObject target;
private Vector3 direction;
private void Start()
{
rb = GetComponent<Rigidbody>();
FlyToTarget();
}

public void FlyToTarget()
{
if (target == null)
{
target = FindObjectOfType<PlayerController>().gameObject;//这个是为了防止石头生成后人物跑脱石头只能原地落下的情况
}
direction = (target.transform.position - transform.position + Vector3.up).normalized;//这个是想让石头往上飞一会,所以加一个向上的单位向量力
rb.AddForce(direction * force, ForceMode.Impulse);//设置力的模式为冲击力
}
}

反击石头回去

反击部分

给石头挂上Attackable的标签,并在Rock里面创建函数代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    private void OnCollisionEnter(Collision other)
{
switch (rockStates)
{
case RockStates.HitPlayer:
if (other.gameObject.CompareTag("Player"))
{
other.gameObject.GetComponent<NavMeshAgent>().isStopped = true;
other.gameObject.GetComponent<NavMeshAgent>().velocity = direction * force;

other.gameObject.GetComponent<Animator>().SetTrigger("Dizzy");
other.gameObject.GetComponent<CharacterStats>().TakeDamage(damage,other.gameObject.GetComponent<CharacterStats>());

rockStates = RockStates.HitNothing;
}
break;
case RockStates.HitEnemy:
if (other.gameObject.GetComponent<Golem>())//GetComponent的方法也有能返回是否存在该组件的函数
{
var otherStats = other.gameObject.GetComponent<CharacterStats>();
otherStats.TakeDamage(damage, otherStats);
Instantiate(breakEffect, transform.position, Quaternion.identity);//生成粒子特效
Destroy(gameObject);
}
break;
}
}
}

为了让石头落地变成HitNothing状态在Rock的FixedUpdate中判断

1
2
3
4
5
6
7
private void FixedUpdate()
{
if (rb.velocity.sqrMagnitude < 1f)
{
rockStates = RockStates.HitNothing;
}
}

为了让人物反击石头回去,改变人物的PlayController中的Hit函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Hit()
{
if (attackTarget.CompareTag("Attackable"))
{
if (attackTarget.GetComponent<Rock>())
{
attackTarget.GetComponent<Rock>().rockStates = Rock.RockStates.HitEnemy;
attackTarget.GetComponent<Rigidbody>().velocity = Vector3.one;//防止变成HitNothing
attackTarget.GetComponent<Rigidbody>().AddForce(transform.forward * 20, ForceMode.Impulse);
}
}
else
{
var targetStats = attackTarget.GetComponent<CharacterStats>();
targetStats.TakeDamage(characterStats, targetStats);
}
}
  • 关于FixedUpdate和Update还是参考https://blog.csdn.net/kasama1953/article/details/52606419

  • 另外发现bug,人物能穿过扔出来的石头。解决方法是给人物挂上刚体,注意要把人物刚体里的is kinematic关掉,这样能使这个刚体不受力的作用,防止它和自动寻路冲突

Partical System部分

创建粒子特效,打回石头人的时候生成粒子特效(上文代码里已经有了这行)

  • 创建粒子特效一般是先把基本效果搞定,之后打开粒子特效里的渲染器把图贴上

第四部分

最后一部分QAQ

敌人UI界面

界面

首先创建画布并把渲染模式调成世界空间

引入2DSprite包,创建image画红色血条

  • 注意项目右上角有个小眼睛,那个可以隐藏或者显示引入的包

之后在红色血条上创建绿色血条,把图像类型改为填充,填充方法改为水平(其实冷却也是这么做的)

再为每个敌人都创建HealthBarPoint

代码

提醒一下代码文件最好不要有空格,否则Unity里虽然创建了文件但是无法编译

创建HealthBarUI代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class HealthBarUI : MonoBehaviour
{
public GameObject healthUIPrefab;

public Transform barPoint;

public bool alwaysVisible;

public float visibleTime;

private float timeLeft;//计时器

Image healthSlider;

Transform UIbar;

Transform cam;//拿到摄像机的位置

CharacterStats currentStats;

private void Awake()
{
currentStats = GetComponent<CharacterStats>();

currentStats.updateHealthBarOnAttack += UpdateHealthBar;
}

private void OnEnable()
{
cam = Camera.main.transform;

foreach(Canvas canvas in FindObjectsOfType<Canvas>())//这个要确保场景里有物体挂了画布组件
{
if(canvas.renderMode == RenderMode.WorldSpace)
{
UIbar = Instantiate(healthUIPrefab, canvas.transform).transform; //注意这里和java相同C#里面不会出现指针的情况,赋值UIbar就是赋值那个物体
healthSlider = UIbar.GetChild(0).GetComponent<Image>();
UIbar.gameObject.SetActive(alwaysVisible);//判断是否一直可见


}
}
}

private void UpdateHealthBar(int currentHealth, int maxHealth)
{
if (currentHealth <= 0 && gameObject)
Destroy(UIbar.gameObject);

UIbar.gameObject.SetActive(true);//无论是否可见。每次攻击都可见
timeLeft = visibleTime;

float sliderPercent = (float)currentHealth / maxHealth;//避免整形相除报错

healthSlider.fillAmount = sliderPercent;
}
private void LateUpdate()
{
if(UIbar != null)
{
UIbar.position = barPoint.position;
UIbar.forward = -cam.forward;

if (timeLeft <= 0 && !alwaysVisible)
UIbar.gameObject.SetActive(false);
else
timeLeft -= Time.deltaTime;
}
}//一帧之后再调用
}

在之前的CharacterStats类新建事件 public event Action<int, int> updateHealthBarOnAttack;的TakeDamage函数里面注册事件 //TODO:Update UI updateHealthBarOnAttack?.Invoke(CurrentHealth, MaxHealth);

人物升级

类CharacterData_SO新增如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[Header("Kill")]
public int killPoint;
[Header("Level")]
public int currentLevel;
public int maxLevel;
public int baseExp;
public int currentExp;
public float levelBuff;
public float LevelMutiplier
{
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);//clamp是夹紧的意思,这里函数是判断一个值在两个数之间
baseExp += (int)(baseExp * LevelMutiplier);

maxHealth = (int)(maxHealth * LevelMutiplier);
currentHealth = maxHealth;

Debug.Log("Level Up!" + currentLevel + "Max Health:" + maxHealth);
}

然后在CharacterStats里面的TakeDamage()两个函数里面新增 if(CurrentHealth <= 0) attacker.characterData.UpdateExp(characterData.killPoint);进行经验更新即可

玩家信息显示

设置UI

创建PlayerHealthUI类,挂在画布上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerHealthUI : MonoBehaviour
{
Text levelText;

Image healthSlider;

Image expSlider;

private void Awake()
{
levelText = transform.GetChild(2).GetComponent<Text>();
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");//注意这里规定了样式,显示为01,02,03
UpdateExp();
UpdateHealth();
}

void UpdateHealth()
{
float sliderPercent = (float)GameManager.Instance.playerStats.CurrentHealth / (float)GameManager.Instance.playerStats.MaxHealth;
healthSlider.fillAmount = sliderPercent;
}

void UpdateExp()
{
float sliderPercent = (float)GameManager.Instance.playerStats.characterData.currentExp / (float)GameManager.Instance.playerStats.characterData.baseExp;
expSlider.fillAmount = sliderPercent;
}

传送

传送门

首先ShaderGraph制作传送门

  • 注意ShaderGraph的保存摁钮在左上角,一定要记得保存
  • ShaderGraph有些Node的AB两个管道是不完全等价的,要多去尝试

创建代码TransitionPoint类的大致框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 OnTriggerStay(Collider other)
{
if (other.CompareTag("Player"))
canTrans = true;
}

private void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player"))
canTrans = false;
}
}

创建代码TransitionDestination的大致框架

1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TransitionDestination : MonoBehaviour
{
public enum DestinationTag
{
ENTER , A , B , C
}

public DestinationTag destinationTag;
}

同场景传送

场景搭建

  • 创建拱门

ProBuilder上面有齿轮的用Alt键加点击打开编辑界面

(ProBuilder的效果其实就相当于小型blender)

  • 把第二个场景搭建好,并记得bake自动寻路

如果场景过暗或者过亮可以参考https://blog.csdn.net/weixin_42654944/article/details/125622914,天空盒也是在这个面板环境选项上调的

URP有特殊限制,一个物体最多有0~8个灯光照射,多了就不会电量,调整要去配置文件URPSetting里面去调

让摄像机快速对齐视图可以GameObject里面的对齐视图

  • 把面板之类的设为预制体

代码

创建SceneController类(用协程是为了不同场景的异步做准备)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.AI;

public class SceneController : Singleton<SceneController>
{

GameObject player;
NavMeshAgent playerAgent;

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;
playerAgent = player.GetComponent<NavMeshAgent>();
playerAgent.enabled = false;
player.transform.SetPositionAndRotation(GetDestination(destinationTag).transform.position,GetDestination(destinationTag).transform.rotation);//identify就是全是0
playerAgent.enabled = true;
yield return null;
}

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;
}
}

在TransitionPoint类里面增加

1
2
3
4
5
6
7
8
private void Update()
{
if (Input.GetKeyDown(KeyCode.E) && canTrans)
{
//TODO: SceneController传送
SceneController.Instance.TransitionToDestination(this);
}
}
  • 注意Agent只要停用一次目标就丢失,再打开需要重新设立目标

不同场景传送

场景首先要放在生成设置内才可加载

使用异步设计,便于后台加载以及保存原游戏场景

在SceneController的Transition函数里面加上

1
2
3
4
5
6
if (SceneManager.GetActiveScene().name != sceneName)
{
yield return SceneManager.LoadSceneAsync(sceneName);//加载出来之后再继续接下来的代码
yield return Instantiate(playerPrefab, GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);
yield break;//帮助我们关闭协程
}

并且在各个Manager里面要加上DontDestroyOnLoad(this);(这里发现一个事情,同名函数父子类如果没标注重写那会当成另一个函数不报错且会覆盖父函数,一定要注意,尽量不要这么用)

保存数据

使用PlayerPrefs配合Json保存

创建SceneManager类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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);//这个后面的true代表是否写入的时候更容易被读懂
PlayerPrefs.SetString(key,jsonData);
PlayerPrefs.Save();
}

public void Load(Object data, string key)
{
if (PlayerPrefs.HasKey(key))
{
JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString(key), data);
}
}
}
  • 具体PlayPres把这个数据存到哪并且有哪些方法可以看官方文档(还是那个小问号)
  • 目前只保存了玩家的血量和经验值,保存其他得自己写

主菜单

创建主菜单UI

  • 画布的渲染模式里的摄像机和屏幕有什么区别

看着是没什么区别的,摄像机相当于沿着摄像机的视野范围给他镀了一层膜,而屏幕是固定在那个位置的。但是由于有的场景要做转场,所以会先设置成摄像机再设置成世界,方便之后摄像机穿过这层膜完成转场效果

代码

创建MainMenu类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MainMenu : MonoBehaviour
{
Button newGameBtn;

Button continueBtn;

Button quitBtn;

private void Awake()
{
newGameBtn = transform.GetChild(1).GetComponent<Button>();
continueBtn = transform.GetChild(2).GetComponent<Button>();
quitBtn = transform.GetChild(3).GetComponent<Button>();

newGameBtn.onClick.AddListener(NewGame);
continueBtn.onClick.AddListener(CotinueGame);
quitBtn.onClick.AddListener(QuitGame);

}

void NewGame()
{
PlayerPrefs.DeleteAll();
//转换场景
SceneController.Instance.TransitionToFirstLevel();
}

void CotinueGame()
{
if(SaveManager.Instance.SceneName != "")
SceneController.Instance.TransitionToLoadGame();
}
void QuitGame()
{
Application.Quit();
}
}

SceneController增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public void TransitionToLoadGame()
{
StartCoroutine(LoadLevel(SaveManager.Instance.SceneName));
}

public void TransitionToFirstLevel()
{
StartCoroutine(LoadLevel("Game"));
}

public void TransitionToMain()
{
StartCoroutine(LoadMain());
}
IEnumerator LoadLevel(string scene)
{
if(scene != "")
{
yield return SceneManager.LoadSceneAsync(scene);
yield return player = Instantiate(playerPrefab,GameManager.Instance.GetEntrance().position, GameManager.Instance.GetEntrance().rotation);

//保存游戏
SaveManager.Instance.SavePlayerData();
yield break;
}
}

IEnumerator LoadMain()
{
yield return SceneManager.LoadSceneAsync("Main");
yield break;
}

在SaveManager里面增加保存场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public void TransitionToLoadGame()
{
StartCoroutine(LoadLevel(SaveManager.Instance.SceneName));
}

public void TransitionToFirstLevel()
{
StartCoroutine(LoadLevel("Game"));
}

public void TransitionToMain()
{
StartCoroutine(LoadMain());
}
IEnumerator LoadLevel(string scene)
{
if(scene != "")
{
yield return SceneManager.LoadSceneAsync(scene);
yield return player = Instantiate(playerPrefab,GameManager.Instance.GetEntrance().position, GameManager.Instance.GetEntrance().rotation);

//保存游戏
SaveManager.Instance.SavePlayerData();
yield break;
}
}

IEnumerator LoadMain()
{
yield return SceneManager.LoadSceneAsync("Main");
yield break;
}

再加上ESC功能回到主菜单

1
2
3
4
if (Input.GetKeyDown(KeyCode.Escape))
{
SceneController.Instance.TransitionToMain();
}

最后让人物刚生成就加载一下PlayerPrefs里面的数据,在Start里面 SaveManager.Instance.LoadPlayerData()

镜头穿过UI

  • 使用TimeLine给MainCamera和Player(用一个AddOverrideTrack改变Player位置)都打上关键帧(别忘了开录制,并且把唤醒时播放去掉)

  • 在动画播放期间添加ActivationTrack关闭EventSystem

在MainMenu的Awake里面添加

1
2
3
4
quitBtn.onClick.AddListener(QuitGame);

director = FindObjectOfType<PlayableDirector>();
director.stopped += NewGame;

给MainMenu类添加新函数

1
2
3
4
void PlayTimeline()
{
director.Play();
}

淡入淡出

创建SceneFader类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SceneFader : MonoBehaviour
{
CanvasGroup canvasGroup;
public float fadeInDuration;
public float fadeOutDuration;


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;
}

Destroy(gameObject);
}

}

之后就各种用淡入淡出这两函数就可

  • 协程其实还是单线程,只不过可以等待到下一帧,防止一帧内这个函数代码就执行完了

生成

  • 在生成设置里面调场景和系统
  • 在生成设置下玩家设置调更详细的参数,比如图标

总结

本来想最后贴一下代码的,但是……真的太麻烦了

最后这次日记给我的警示就是

写详细的日志太麻烦了,下次就算写也不会写这么长了