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()

posted @   StaDark  阅读(100)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· [翻译] 为什么 Tracebit 用 C# 开发
· 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
· RFID实践——.NET IoT程序读取高频RFID卡/标签
点击右上角即可分享
微信分享提示