学习目标:
可能有人看到我前面截图的场景中并没有将设置为预设体的Enemy01-03拖入场景中,场景却能持续不断的生成敌人,因为制作了敌人管理器,确保敌人能够一直生成。今天研究一下怎么创建敌人管理器。
学习内容:
先创建一个EnemyManager然后以及一个同名脚本。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyManager : Singleton<EnemyManager>
{
public int WaveNumber => waveNumber;
public float TimeBetweenWaves => timeBetwwenWave;
[SerializeField] bool spawnEnemy = true;
[SerializeField] GameObject waveUI;
[SerializeField] GameObject[] enemyPrefabs;
[SerializeField] float timeBetweenSpawn = 1f;
[SerializeField] float timeBetwwenWave = 1f;
[SerializeField] int minEnemyAmount = 4;
[SerializeField] int maxEnemyAmount = 10;
int waveNumber = 1;
int enemyAmount;
WaitForSeconds waitTimeBetwenSpawns;
WaitForSeconds waitTimeBetweenWait;
WaitUntil waitUntilNoEnemy; //等待直到满足某个条件
List<GameObject> enemyList;
protected override void Awake()
{
base.Awake();
waitTimeBetwenSpawns = new WaitForSeconds(timeBetweenSpawn);
waitTimeBetweenWait = new WaitForSeconds(timeBetwwenWave);
waitUntilNoEnemy = new WaitUntil(() => enemyList.Count == 0); //这里用一个Lamada表达式实现委托
enemyList = new List<GameObject>();
}
IEnumerator Start()
{
while (spawnEnemy)
{
yield return waitUntilNoEnemy;
waveUI.SetActive(true);
yield return waitTimeBetweenWait;
waveUI.SetActive(false);
StartCoroutine(nameof(RandomlySpawnCoroutine));
}
}
IEnumerator RandomlySpawnCoroutine()
{
enemyAmount = Mathf.Clamp(enemyAmount, minEnemyAmount + waveNumber / 3, maxEnemyAmount);
for (int i = 0; i < enemyAmount; i++)
{
enemyList.Add(PoolManager.Release(enemyPrefabs[Random.Range(0, enemyPrefabs.Length)]));
yield return waitTimeBetwenSpawns;
}
waveNumber++;
}
public void RemoveList(GameObject enemy) => enemyList.Remove(enemy);
}
创建完成之后,我们再把参数,以及之前做好的Enemy预制体拖上去
然后我们创建波数UI
给WavaUI一个脚本并且成绩一个动画
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class WaveUI : MonoBehaviour
{
Text waveText;
private void Awake()
{
GetComponent<Canvas>().worldCamera = Camera.main;
waveText = GetComponentInChildren<Text>();
}
private void OnEnable()
{
waveText.text = "- WAVE" + EnemyManager.Instance.WaveNumber + " -";
}
}
在Enemy脚本中
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : Character
{
[SerializeField] int deathEnergyBouns = 3;
public override void Die()
{
PlayerEnergy.Instance.Obtain(deathEnergyBouns);
EnemyManager.Instance.RemoveList(gameObject);
base.Die();
}
}
制作音效管理器:
新创建一个Audio Manager,并且给它一个带Audio source的组件,一个用来播放背景音,一个用来播放需要播放的音乐
最后再给父对象一个同名脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AudioManager : PersistentSingleTon<AudioManager>
{
[SerializeField] AudioSource sFXPlayer;
const float MIN_PITCH = 0.9F;
const float MAX_PITCH = 1.1F;
public void PlaySFX(AudioData audioData)
{
sFXPlayer.PlayOneShot( audioData.audioClip, audioData.volume);
}
//适合连续重复播放的音效
public void PlayRandomSFX(AudioData audioData)
{
//音高属性
sFXPlayer.pitch = Random.Range(MIN_PITCH, MAX_PITCH);
PlaySFX(audioData);
}
public void PlayRandomSFX(AudioData[] audioData)
{
PlayRandomSFX(audioData[Random.Range(0, audioData.Length)]);
}
}
[System.Serializable]
public class AudioData
{
public AudioClip audioClip;
public float volume;
}
我们设置为单例,并给新创建一个类,这些都是为了方便其它脚本调用它
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
[SerializeField] float moveSpeed = 10f;
[SerializeField] protected Vector2 moveDirection;
[SerializeField] float damage;
[SerializeField] AudioData[] hitSFX;
[SerializeField] GameObject hitVFX;
protected GameObject target;
protected virtual void OnEnable()
{
StartCoroutine(MoveDirectly());
}
IEnumerator MoveDirectly()
{
while (gameObject.activeSelf)
{
transform.Translate(moveDirection * moveSpeed * Time.deltaTime);
yield return null;
}
}
protected virtual void OnCollisionEnter2D(Collision2D collision)
{
if(collision.gameObject.TryGetComponent<Character>(out Character character)) //判断碰撞的游戏对象是否为Character类,所消耗的性能更少
{
character.TakeDamage(damage);
var contactPoint = collision.GetContact(0); //得到collision的第一个接触点
PoolManager.Release(hitVFX, contactPoint.point, Quaternion.LookRotation(contactPoint.normal)); //contactPoint.normal法线方向,某个顶点在3D空间中的朝向
AudioManager.Instance.PlayRandomSFX(hitSFX);
gameObject.SetActive(false);
}
}
}
还有是Character相关的脚本上
using System.Collections;
using UnityEngine;
public class Character : MonoBehaviour
{
[SerializeField] public GameObject deathVFX;
[SerializeField] AudioData[] deathSFX;
[Header("--- HEALTH ---")]
[SerializeField] protected float maxHealth;
protected float health;
[SerializeField] StateBar onHeadHealthBar;
[SerializeField] bool showOnHealthBar = true;
protected virtual void OnEnable()
{
health = maxHealth;
if (showOnHealthBar)
{
ShowOnHealthBar();
}
else
{
HideOnHealthBar();
}
}
public void ShowOnHealthBar()
{
onHeadHealthBar.gameObject.SetActive(true);
onHeadHealthBar.Initialize(health, maxHealth);
}
public void HideOnHealthBar()
{
onHeadHealthBar.gameObject.SetActive(false);
}
public virtual void TakeDamage(float damage)
{
health -= damage;
if (showOnHealthBar && gameObject.activeSelf)
{
onHeadHealthBar.UpdateStates(health, maxHealth);
}
if(health <= 0)
{
Die();
}
}
public virtual void Die()
{
health = 0;
AudioManager.Instance.PlayRandomSFX(deathSFX);
PoolManager.Release(deathVFX, transform.position, Quaternion.identity);
gameObject.SetActive(false);
}
public virtual void RestoreHealth(float value)
{
if (health == maxHealth)
return;
health = Mathf.Clamp(health + value, 0, maxHealth);
if (showOnHealthBar)
{
onHeadHealthBar.UpdateStates(health, maxHealth);
}
}
protected IEnumerator HealthPercentageCoroutine(WaitForSeconds waitTime,float percent)
{
while(health < maxHealth)
{
yield return waitTime;
RestoreHealth(percent * maxHealth);
}
}
protected IEnumerator DamageOverTime(WaitForSeconds waitTime, float percent)
{
while (health > 0)
{
yield return waitTime;
RestoreHealth(percent * maxHealth);
}
}
}
using System.Collections;
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class Player : Character
{
[SerializeField] StateBar_HUD stateBar_HUD;
[SerializeField] bool genearatedHealth = true;
[SerializeField] float healthGenerateTime;
[SerializeField,Range(0f,1f)] float healthRegeneatePercent;
[Header("--- INPUT ---")]
[SerializeField] PlayerInput input;
[Header("--- MOVE ---")]
[SerializeField] float accelarationTime = 3f;
[SerializeField] float decelarationTime = 3f;
[SerializeField] float moveSpeed = 10f;
[SerializeField] float moveRotatinAngle = 50f;
[SerializeField] float paddingX = 0.2f;
[SerializeField] float paddingY = 0.2f;
[SerializeField] float fireInterval= 0.2f;
float t;
Vector2 previousVelocity;
Quaternion previousRotation;
[Header("--- FIRE ---")]
[SerializeField] GameObject projectTile1;
[SerializeField] GameObject projectTile2;
[SerializeField] GameObject projectTile3;
[SerializeField] AudioData projectileLaunchSFX;
[SerializeField, Range(0, 2)] int weaponPower = 0;
[SerializeField] Transform muzlleMiddle;
[SerializeField] Transform muzzleTop;
[SerializeField] Transform muzzleBottom;
Rigidbody2D rigi2D;
new Collider2D collider;
[Header("--- COROUTINE ---")]
Coroutine moveCoroutine;
Coroutine healthRegenerateCoroutine;
WaitForSeconds waitForFireInterval;
WaitForSeconds waitHealthGeneratedTime;
[Header("--- DODGE ---")]
[SerializeField] int dodgeEnergyCost = 25;
[SerializeField] float maxRoll = 360f;
[SerializeField] float rollSpeed = 360f;
[SerializeField] Vector3 dodgeScale = new Vector3(0.5f, 0.5f, 0.5f);
[SerializeField] AudioData dodgeSFX;
bool isDodging = false;
float currentRoll;
float dodgeDuration;
private void Awake()
{
rigi2D = GetComponent<Rigidbody2D>();
collider = GetComponent<Collider2D>();
dodgeDuration = maxRoll / rollSpeed;
}
protected override void OnEnable()
{
//增加委托
base.OnEnable();
input.onMove += Move;
input.onStopMove += StopMove;
input.onFire += Fire;
input.onStopFire += StopFire;
input.onDodge += Dodge;
}
private void OnDisable()
{
//取消委托
input.onMove -= Move;
input.onStopMove -= StopMove;
input.onFire -= Fire;
input.onStopFire -= StopFire;
input.onDodge -= Dodge;
}
void Start()
{
rigi2D.gravityScale = 0f;
waitForFireInterval = new WaitForSeconds(fireInterval);
waitHealthGeneratedTime = new WaitForSeconds(healthGenerateTime);
stateBar_HUD.Initialize(health, maxHealth);
input.EnableGamePlay(); //激活动作表
}
public override void TakeDamage(float damage)
{
base.TakeDamage(damage);
stateBar_HUD.UpdateStates(health, maxHealth);
if (gameObject.activeSelf)
{
if(healthRegenerateCoroutine != null)
{
StopCoroutine(healthRegenerateCoroutine);
}
healthRegenerateCoroutine = StartCoroutine(HealthPercentageCoroutine(waitHealthGeneratedTime, healthRegeneatePercent));
}
}
public override void RestoreHealth(float value)
{
base.RestoreHealth(value);
stateBar_HUD.UpdateStates(health, maxHealth);
}
public override void Die()
{
stateBar_HUD.UpdateStates(0f, maxHealth);
base.Die();
}
#region WALK
void Move(Vector2 moveInput) //就是你输入信号的二维值
{
//Vector2 moveAmount = moveInput * moveSpeed;
//rigi2D.velocity = moveAmount;
if (moveCoroutine != null)
{
StopCoroutine(moveCoroutine);
}
Quaternion moveRotation = Quaternion.AngleAxis(moveRotatinAngle * moveInput.y,Vector3.right); //right即红色的X轴,初始旋转角度
moveCoroutine=StartCoroutine(MoveCoroutine(accelarationTime,(moveInput.normalized * moveSpeed),moveRotation));
StartCoroutine(nameof(MovePositionLimitCoroutine));
}
void StopMove()
{
//rigi2D.velocity = Vector2.zero;
if (moveCoroutine != null)
{
StopCoroutine(moveCoroutine);
}
moveCoroutine = StartCoroutine(MoveCoroutine(decelarationTime,Vector2.zero, Quaternion.identity));
StopCoroutine(nameof(MovePositionLimitCoroutine));
}
IEnumerator MoveCoroutine(float time,Vector2 moveVelocity,Quaternion moveRotation)
{
t = 0f;
previousVelocity = rigi2D.velocity;
previousRotation = transform.rotation;
while(t< 1f)
{
t += Time.fixedDeltaTime / time;
rigi2D.velocity = Vector2.Lerp(previousVelocity, moveVelocity,t);
transform.rotation = Quaternion.Lerp(previousRotation, moveRotation,t);
yield return new WaitForFixedUpdate();
}
}
IEnumerator MovePositionLimitCoroutine()
{
while (true)
{
transform.position = ViewPort.Instance.PlayerMoveablePosition(transform.position,paddingX,paddingY);
yield return null;
}
}
#endregion
#region FIRE
void Fire()
{
StartCoroutine(nameof(FireCoroutine));
}
void StopFire()
{
StopCoroutine(nameof(FireCoroutine));
}
IEnumerator FireCoroutine()
{
while (true)
{
switch (weaponPower)
{
case 0:
PoolManager.Release(projectTile1, muzlleMiddle.position, Quaternion.identity);
break;
case 1:
PoolManager.Release(projectTile1, muzzleTop.position, Quaternion.identity);
PoolManager.Release(projectTile1, muzzleBottom.position, Quaternion.identity);
break;
case 2:
PoolManager.Release(projectTile1, muzzleTop.position, Quaternion.identity);
PoolManager.Release(projectTile1, muzlleMiddle.position, Quaternion.identity);
PoolManager.Release(projectTile1, muzzleBottom.position, Quaternion.identity);
break;
default:break;
}
AudioManager.Instance.PlayRandomSFX(projectileLaunchSFX);
yield return waitForFireInterval;
}
}
#endregion
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
[SerializeField] float moveSpeed = 10f;
[SerializeField] protected Vector2 moveDirection;
[SerializeField] float damage;
[SerializeField] AudioData[] hitSFX;
[SerializeField] GameObject hitVFX;
protected GameObject target;
protected virtual void OnEnable()
{
StartCoroutine(MoveDirectly());
}
IEnumerator MoveDirectly()
{
while (gameObject.activeSelf)
{
transform.Translate(moveDirection * moveSpeed * Time.deltaTime);
yield return null;
}
}
protected virtual void OnCollisionEnter2D(Collision2D collision)
{
if(collision.gameObject.TryGetComponent<Character>(out Character character)) //判断碰撞的游戏对象是否为Character类,所消耗的性能更少
{
character.TakeDamage(damage);
var contactPoint = collision.GetContact(0); //得到collision的第一个接触点
PoolManager.Release(hitVFX, contactPoint.point, Quaternion.LookRotation(contactPoint.normal)); //contactPoint.normal法线方向,某个顶点在3D空间中的朝向
AudioManager.Instance.PlayRandomSFX(hitSFX);
gameObject.SetActive(false);
}
}
}
场景加载器:
我们还要写一个SceneLoader用于不同场景
复制粘贴我们的GamePlay场景然后重新取名MainMenu,将不需要的gameobject删除掉
先简单为它添加一个Button,
给它一个点击事件
然后创建一个覆盖全景的图片这里你看不到是因为我已经把它设置为透明度为0,目的是从0到1的透明度实现 场景的淡入淡出。
我们新创建一个脚本实现按钮的点击事件脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class SceneLoader : PersistentSingleTon<SceneLoader>
{
[SerializeField] Image transitionImage;
[SerializeField] float fadeTime = 3.5f;
Color color;
const string GAMEPLAY = "GamePlay";
void Load(string sceneName)
{
SceneManager.LoadScene(sceneName);
}
IEnumerator LoadCoroutine(string sceneName)
{
//Time.unscaledDeltaTime不会受到Time.deltaTime的影响
//为了防止场景加载太快,我们的场景淡入淡出动画没加载完成,这里我们需要等待动画播放完成后
//才能继续开启场景
var loadingOperation = SceneManager.LoadSceneAsync(sceneName);
loadingOperation.allowSceneActivation = false; //用来设置加载好的场景是否为激活状态,在后台也可以加载
transitionImage.gameObject.SetActive(true);
while(color.a < 1f)
{
color.a = Mathf.Clamp01(color.a + Time.unscaledDeltaTime / fadeTime);
transitionImage.color = color;
yield return null;
}
loadingOperation.allowSceneActivation = true;
while (color.a > 0f)
{
color.a = Mathf.Clamp01(color.a - Time.unscaledDeltaTime / fadeTime);
transitionImage.color = color;
yield return null;
}
transitionImage.gameObject.SetActive(false);
}
public void LoadGamePlayScene()
{
StartCoroutine(LoadCoroutine(GAMEPLAY));
}
}
参数搞好然后默认将图片设置为false
学习产出:
点下按钮后。