unity学习笔记(三)
综合练习小案例
玩家控制
基本流程
-
设定移速(全局,以便在unity界面中直接修改)(如
public float speed = 5;
) -
将移动单独封装成方法
-
在移动方法中完成获取输入、设置移动动画、设置移动时朝向以及移动角色
private void Move() { //获取输入 int inputX = (int)UnityEngine.Input.GetAxisRaw("Horizontal"); int inputY = (int)UnityEngine.Input.GetAxisRaw("Vertical"); //设置动画 bool isMoving = (inputX!= 0 || inputY!= 0); animator.SetBool("Move", isMoving); //设置角色的朝向 if (inputX > 0) transform.localScale = new Vector3(5, 5, 1); else if (inputX < 0) transform.localScale = new Vector3(-5, 5, 1); //移动角色 Vector3 moveDirection = new Vector3(inputX, inputY, 0); transform.Translate(moveDirection * moveSpace * Time.deltaTime); }
要点:
- 最好给玩家同时添加碰撞体(人形的话椭圆形最好)和刚体,并设置为触发器,以在节约资源的情况下完成与其他物体的互动(物理碰撞,被攻击死亡之类的)
- 当获取X,Y轴输入时,
GetAxis()
函数传递的值类型为float
,移动效果为有惯性;
GetAxisRaw()
函数传递的值类型为int
,移动效果为无惯性。注意区分并转换类型
发射子弹
基本流程
-
在角色上创建一个空物体作为子弹发生点
-
需要给操作角色封装一个攻击方法的同时,给子弹一个运行脚本
-
攻击方法:
实例化子弹(gemPrefab
需要在unity中选中) -> 子弹发射(移动)public GameObject gemPrefab; public Transform gemShootPoint; public float shootCD = 1; private float shootCDTimer; private void Attack() { //子弹冷却 if (shootCDTimer > 0) { shootCDTimer -= Time.deltaTime; //如果子弹冷却时间未到,则不发射子弹 return; } //按下空格发射子弹 if (UnityEngine.Input.GetKeyDown(KeyCode.Space)) { //设置子弹冷却时间 shootCDTimer = shootCD; //实例化一个Gem子弹 GameObject gem = GameObject.Instantiate(gemPrefab); //设置子弹的位置 gem.transform.position = gemShootPoint.position; //设置子弹的方向 bool isRight = transform.localScale.x == 5; Vector3 moveDir = isRight? Vector3.right * 5 : Vector3.left * 5; gem.GetComponent<Gem>().Init(moveDir); } }
-
子弹脚本:
设置子弹基本属性(速度、销毁时间)public class Gem : MonoBehaviour { public float moveSpeed = 5f; //子弹移动方向 private Vector3 moveDir; public float destroyTime = 2; private float destroyTimer; //初始化 public void Init(Vector3 dir) { moveDir = dir; destroyTimer = destroyTime; } void Update() { //gem子弹移动 transform.Translate(moveDir * moveSpeed * Time.deltaTime, Space.World); //gem子弹销毁 if (destroyTimer <= 0) { Debug.Log("Gem Destroyed"); Destroy(gameObject); } //gem子弹销毁计时器 else { destroyTimer -= Time.deltaTime; } } }
怪物生成
基本流程
- 确定好怪物生成的坐标点位
- 确定好怪物生成的时间间隔(CD)
public class MonsterManager : MonoBehaviour
{
//序列化(将数据可视化到unity的Inspector面板中)
[System.Serializable]
//怪物生成点
public class MonsterSpawnPoint
{
//生成点
public Transform spawnPoint;
//怪物预制体
public GameObject monsterPrefab;
//生成间隔
public float spawnInterval;
//计时器(需要非序列化)
private float spawnTimer;
public void Update(float deltaTime)
{
//更新计时器
spawnTimer -= Time.deltaTime;
if (spawnTimer <= 0)
{
spawnTimer = spawnInterval;
//生成怪物
GameObject monster = GameObject.Instantiate(monsterPrefab);
//将不同点生成的怪物归类到对应点下
monster.transform.SetParent(spawnPoint);
//设置怪物位置
monster.transform.position = spawnPoint.position;
}
}
}
//怪物生成点数组
public MonsterSpawnPoint[] spawnPoints;
private void Update()
{
//更新所有生成点
for (int i = 0; i < spawnPoints.Length; i++)
{
spawnPoints[i].Update(Time.deltaTime);
}
}
}
创建MonsterManager
后可以再在其下设置好预设的点位Points
怪物逻辑
移动逻辑
基本流程
-
确定怪物移动逻辑是当玩家存在时,向玩家移动
将Player
设置为单例模式//单例模式:通过将当前Player对象赋值给静态变量instance,确保游戏中只有一个Player实例,并且可以通过Player.instance全局访问。 public static Player instance; //当游戏开始时,将当前Player对象设置为静态变量instance。 //优点:无论在游戏的任何地方,我们都可以通过Player.instance来访问这个唯一的Player实例,从而更方便地进行全局访问和控制 //初始化:在游戏开始前进行必要的初始化操作,确保Player对象在游戏中的唯一性和可访问性。 private void Awake() { instance = this; }
Monster
检查玩家是否存在,不存在的话就不执行移动逻辑if (Player.instance == null) return;
-
怪物执行向玩家移动的逻辑
//利用向量知识,将怪物的方向始终对准玩家方向 Vector3 moveDir = (Player.instance.transform.position - transform.position).normalized; //怪物移动 transform.Translate(Time.deltaTime * moveSpeed * moveDir, Space.World); //判断怪物移动时的左右方向 bool isRight = transform.position.x > Player.instance.transform.position.x; //根据移动时单独左右方向旋转贴图 if (isRight) transform.localScale = new Vector3(-5, 5, 1); else transform.localScale = new Vector3(5, 5, 1);
受击死亡逻辑
基本流程
-
为怪物添加HP变量,并设置好触发器事件函数并添加死亡动画事件
public int HP; //动画控制器 public Animator animator; private void OnTriggerEnter2D(Collider2D collision) { if (HP == 0) return; //接触子弹 if (collision.gameObject.CompareTag("Bullet")) { //子弹碰撞后,怪物生命值减一,子弹消失 Destroy(collision.gameObject); HP-=1; //HP归零时触发 if (HP == 0) { animator.SetTrigger("Death"); } } } #region 动画事件 //死亡动画结束 private void OnDeathAnimationEnd() { //对象消亡 Destroy(gameObject); } #endregion
-
为怪物设置好死亡动画,并在动画最后一阵添加动画事件(选定脚本里写好的)
设置动画状态,并把条件设置为触发器(无后摇)
-
将怪物添加上碰撞体组件,并勾选触发器选项
-
将子弹添加刚体和碰撞体,锁定三个方向的轴并加上
“Bullet”
标签
鼠标指向为攻击方向
基本流程
-
通过摄像机(
Camera
获取鼠标的坐标),再将角色朝向翻转逻辑和攻击方向逻辑改写//使用new关键字明确地表示camera字段是当前类特有的,与基类中的任何同名字段无关。 public new Camera camera; void Update() { //获取鼠标位置 Vector3 mousePos = camera.ScreenToWorldPoint(Input.mousePosition); mousePos.z = 0; Move(mousePos); Attack(mousePos); } private void Move(Vector3 mousePos) { //获取输入 int inputX = (int)UnityEngine.Input.GetAxisRaw("Horizontal"); int inputY = (int)UnityEngine.Input.GetAxisRaw("Vertical"); //设置动画 bool isMoving = (inputX!= 0 || inputY!= 0); animator.SetBool("Move", isMoving); //设置角色的朝向 if (mousePos.x > transform.position.x) transform.localScale = new Vector3(5, 5, 1); else if (mousePos.x < transform.position.x) transform.localScale = new Vector3(-5, 5, 1); //移动角色 Vector3 moveDirection = new Vector3(inputX, inputY, 0); transform.Translate(moveDirection * moveSpace * Time.deltaTime); } private void Attack(Vector3 mousePos) { //子弹冷却 if (shootCDTimer > 0) { shootCDTimer -= Time.deltaTime; //如果子弹冷却时间未到,则不发射子弹 return; } //鼠标左键按下发射子弹,持续按下可以持续发射子弹 if (Input.GetMouseButton(0)) { //设置子弹冷却时间 shootCDTimer = shootCD; //实例化一个Gem子弹 GameObject gem = GameObject.Instantiate(gemPrefab); //设置子弹的位置 gem.transform.position = gemShootPoint.position; //设置子弹的方向 Vector3 moveDir = (mousePos - transform.position).normalized; gem.GetComponent<Gem>().Init(moveDir); } }
玩家生命值以及状态(模拟UI)
基本流程
-
为解决人物移动时,
Camera
会带着UI一起翻转的问题,需要将Camare
单独列出并使用脚本的方法让Camera
跟随玩家视角
-
为保证能稳定获取玩家最后一帧的位置,所以使用
LateUpdate
public Transform target; private void LateUpdate() { Vector3 pos = target.position; //避免摄像机与其他素材重叠 pos.z = -10; transform.position = pos; }
可新建一个
CameraPoint
在玩家身上,用于挂起target
-
给玩家新建一个HP字段并定义属性,后继续设置一个HP精灵管理器以及HP状态精灵数组
//声明一个HpSpriteRenderer字段,用于渲染角色的血条 public SpriteRenderer HpSpriteRenderer; //声明一个HpSprites字段,用于存储角色的血条图片 public Sprite[] HpSprites; //[SerializeField]:这是一个属性(Attribute),用于指示Unity在Inspector窗口中显示私有字段hp,即使它是私有的。 //private int hp:声明一个私有的整型变量hp,用于存储角色的生命值。 [SerializeField] private int hp; //声明一个公共属性HP,用于外部访问和修改私有字段hp。 public int HP { //返回私有字段hp的值。 get => hp; //用于设置私有字段hp的值,并在设置值时更新HpSpriteRenderer的精灵图像。 set { //将传入的值value赋给私有字段hp hp = value; HpSpriteRenderer.sprite = HpSprites[hp]; } } private void Awake() { instance = this; //血量初始化 HP = HpSprites.Length - 1; }
-
在unity中拖入对应组件
玩家受伤及死亡
基本流程
-
在怪物的脚本下设置好玩家触发到怪物后的事件
private void OnTriggerEnter2D(Collider2D collision) { //接触玩家 //如果发生碰撞的物体Tag为“Player”的话,调用对应组件的Hurt()方法 if (collision.gameObject.CompareTag("Player")) { collision.gameObject.GetComponent<Player>().Hurt(); } }
-
在玩家脚本下写好受伤方法,死亡方法以及死亡动画事件
[SerializeField] private int hp; public int HP { get => hp; set { hp = value; HpSpriteRenderer.sprite = HpSprites[hp]; //将和hp归零判断放在字段中更方便 if (hp == 0) { Death(); } } } public void Hurt() { if (HP <= 0) return; HP-=1; } private void Death() { //触发死亡动画条件 animator.SetTrigger("Death"); } #region 动画事件 //死亡动画结束 private void OnDeathAnimationEnd() { //隐藏玩家组件 gameObject.SetActive(false); } #endregion
编辑好死亡动画后,在动画最后一帧设置动画事件,选定
OnDeathAnimationEnd()