综合练习小案例
玩家控制
基本流程
-
设定移速(全局,以便在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赋给私有字段hphp = 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()