RPG UNITY实战

1.移动

//移动,分别是x轴和y轴的速度
rb.velocity = new Vector2 (horizontal * moveSpeed, rb.velocity.y);

2.私有且可以在检查器浏览和修改此值

    //私有且可以在检查器浏览和修改此值
    [SerializeField] private float moveSpeed;
    [SerializeField] private float jumpForce;

3.切分sprite图

3.1.当sprite和中心点不匹配

  • 创建空子物体,此子物体加上Sprite Renderer组件把Sprite放在子物体上,移动子物体来使sprite和中心点对齐; 

4.如何允许对象只在地面上才可以跳跃

1.找到地面和对象的距离:对象往下划线,如果超越地面就可得到距离,这个距离值设为公开;

  • //不需要启动游戏,直接就会划线
    [SerializeField] private float GroundCheckDistance;
    //不需要启动游戏,直接就会划线
    //检测距离地面的数值
    private void OnDrawGizmos()
    {
        Gizmos.DrawLine(transform.position, new Vector2(transform.position.x, transform.position.y - GroundCheckDistance));
    }

2. 使用射线检测是否在地面上

    //检测是否在地面
    [SerializeField] private LayerMask groundLayer;
    [SerializeField] private bool isGround;

    private void GroundCollisionCheck()
    {
        isGround = Physics2D.Raycast(transform.position, Vector2.down, groundCheckDistance, groundLayer);
    }

5.角色粘墙问题

  • 给墙体创建物理材质,摩擦力设为0即可解决

6.冲刺:点击左右键+冲刺键,在一定时间内速度提升

  • 在冲刺时间内播放冲刺动画且速度提升;
  • 设置一个冲刺时间初始值,按冲刺键获取这个值,Update函数用获得值-=Time.deltaTime

7.非玩家角色检测是否在地面边缘

  • 创建一个Transform组件的对象作为游戏物体的子对象,调整Transform位置在游戏物体脚下
    [SerializeField] protected Transform groundCheck;
    protected virtual void OnDrawGizmos()
    {
        Gizmos.DrawLine(groundCheck.position, new Vector2(groundCheck.position.x, groundCheck.position.y - groundDistance));
    }

7.P40:可以是攻击时完全不移动; 

8.调色板

8.1.创建调色板并设置不同的图层

给Ground层添加Tilemap和Composition碰撞器,tilemap设置复用选项

背景层图层设置-1

8.2.调色板选项和使用

8.3.摄像机

9.做背景:选择图片2张及以上拷贝3份,依次排列,一个快一个慢 ,超出图片尺寸距离,移动图片位置

计算图片超出距离需要改

public class ParallaxBackGround : MonoBehaviour
{
    private GameObject cam;
    [SerializeField] private float parallaxEffect;
    private float xPosition; 
    private float len;
    void Start()
    {
        cam = GameObject.Find("Main Camera");
        xPosition = transform.position.x;
        len =GetComponent<SpriteRenderer>().bounds.size.x;//获取图片大小
    }

    // Update is called once per frame
    void Update()
    {
        float distanceToMove = cam.transform.position.x * parallaxEffect;
        float distanceMove = cam.transform.position.x * (1 - parallaxEffect);
        //原本坐标加上摄像机坐标*视差
        transform.position = new Vector3(xPosition + distanceToMove, transform.position.y);
        //动画不缺失
        if (distanceMove > xPosition + len)
            xPosition += len;
        else if (distanceMove < xPosition - len)
            xPosition -= len;
    }
}

10.攻击检测

在基类写一个收到伤害函数

    public void Damage()
    {
        Debug.Log(gameObject.name + " was damage ");
    }

基类需要一个Transform对象和检测半径

    public Transform _attackCheck;
    public float _attackCheckRadius;
    protected virtual void OnDrawGizmos()
    {
        Gizmos.DrawWireSphere(_attackCheck.position, _attackCheckRadius);
    }

在攻击动画添加事件

OverlapCircleAll检测圆内的所有碰撞体

    void AttackTrigger()
    {
        Collider2D[] collider2Ds = Physics2D.OverlapCircleAll(_player._attackCheck.position, _player._attackCheckRadius);
        foreach(var hit in  collider2Ds)
        {
            if(hit.GetComponent<Enemy>() != null)
                hit.GetComponent<Enemy>().Damage();
        }
    }

11.不同图层是否做碰撞检测

12.受击闪光

创建一个材质,设置为白色并且shader为GUI

开始使用默认材质,flashTime时间后使用时候添加的材质

    private SpriteRenderer sr;
    [Header("Flash FX")]
    private Material originalMaterial;
    [SerializeField] private Material hitMaterial;
    [SerializeField] private float flashTime;
    private void Start()
    {
        sr = GetComponent<SpriteRenderer>();
        originalMaterial = sr.material;
    }

    private IEnumerator  FlashFX()
    {
        sr.material = hitMaterial;
        yield return  new WaitForSeconds(flashTime);
        sr.material = originalMaterial;
    }

13.受击后退

在攻击动画中添加事件

添加一个子对象,使用此对象的位置和Gizmos.DrawWireSphere来画圆,调整圆大小

如何检测

14.受击反击 

1.敌人创建一个被反击击晕状态

public class Skeleton_Stunned : EnemyState
{
    Skeleton_Enemy _enemy;
    public Skeleton_Stunned(Skeleton_Enemy enemy, EnemyStateMachine stateMachine, string stateName) : base(enemy, stateMachine, stateName)
    {
        _enemy = enemy;
    }

    public override void Enter()
    {
        base.Enter();
        stateTime =1;//击晕时间
        _rb.velocity = new Vector2(_enemy.StunnedDir.x * -_enemy.facingDir, _enemy.StunnedDir.y);//击退
        _enemy._entityFX.InvokeRepeating("RedColorBlink", 0, 0.2f);//闪光
    }

    public override void Exit()
    {
        base.Exit();
        _enemy._entityFX.Invoke("CancelRedBlink",0);//取消闪光
    }

    public override void Update()
    {
        base.Update();
        if(stateTime < 0)
            _enemyStateMachine.ChangeState(_enemy.idleState);
    }   
}

格挡反击逻辑:玩家就如格挡动画,格挡成功进入反击动画,敌人进入击晕动画

15.冲刺留下影像攻击

1.在冲刺技能enter()创建影像

    public override void Enter()
    {
        base.Enter();
        SkillManager._instance._clone.CreateClone(_player.transform);//clone
    }

2.设置Animotor,动画内有AttackTrigger事件,检测攻击范围敌人,并使敌人受击;

3.创建预设体,设置预设体寻找最近的敌人面向它,使用技能管理器决定能否攻击,随机是用3种攻击的一种;Update使物体的颜色逐渐变淡,为零删除对象

public class Clone_Skill : Skill
{
    [SerializeField] private GameObject _clonePrefab;//可用预设体
    [SerializeField] private bool _canAttack;

    public void CreateClone(Transform cloneTrans)
    {
        GameObject newClone = Instantiate(_clonePrefab);
        newClone.GetComponent<Clone_Skill_Control>().SetupClone(cloneTrans,_canAttack);
    }
}
public class Clone_Skill_Control : MonoBehaviour
{
    private SpriteRenderer sr;
    private Animator _anim;
    [Header("Collider info")]
    [SerializeField] private Transform _attackCheck;
    [SerializeField] private float _attackCheckRadius;

    [SerializeField] private float _cloneDuration;
    private float _cloneTime;
    [SerializeField] private float _cloneLoosingSpeed;
    private void Awake()
    {
        sr = GetComponent<SpriteRenderer>();
        _anim = GetComponent<Animator>();
    }
    //颜色逐渐变淡
     void Update()
    {
        _cloneTime -= Time.deltaTime;
        if (_cloneTime < 0)
        {
            sr.color = new Color(1, 1, 1, sr.color.a - (Time.deltaTime * _cloneLoosingSpeed));
        }
    }
    public void SetupClone(Transform newTransform, bool canAttack)
    {
        _cloneTime = _cloneDuration;
        transform.position = newTransform.position;

        //选择最近敌人,并且面向他
        float minDistance = float.MaxValue;
        Collider2D minDIsCollider = null;
        Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 25);
        foreach (var collider in colliders)
        {
            if(collider.GetComponent<Enemy>() != null)
            {
                float current = Vector2.Distance(transform.position, collider.transform.position);
                if (current < minDistance)
                {
                    minDistance = current;
                    minDIsCollider = collider;
                }
            }
        }
        if(minDIsCollider != null) 
        {
            if (minDIsCollider.transform.position.x < transform.position.x)
                transform.Rotate(0, 180, 0);
        }
        //有技能管理器决定能否攻击
        if(canAttack)
            _anim.SetInteger("AttackCounter", Random.Range(1, 3));
    }
    private void AnimationTriggers()
    {
    }
    private void AttackTrigger()
    {
        Collider2D[] collider2Ds = Physics2D.OverlapCircleAll(_attackCheck.position, _attackCheckRadius);
        foreach (var hit in collider2Ds)
        {
            if (hit.GetComponent<Enemy>() != null)
                hit.GetComponent<Enemy>().Damage();
        }
    }
}

16.投掷技能

16.1.创建一个剑并投掷

1.Animator设置:按鼠标右键(Aim参数 == true)进入瞄准动画,松开(Aim参数 == false)进入投掷动画;

2.逻辑

public class PlayerAimSwordState : PlayerState
{
    public PlayerAimSwordState(Player player, PlayerStateMachine playerStateMachine, string animName) : base(player, playerStateMachine, animName)
    {
    }

    //事件创建剑
    public override void Enter()
    {
        base.Enter();
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();
        if(Input.GetKeyUp(KeyCode.Mouse1)) 
            _playerStateMachine.ChangeState(_player._idleState);
    }
}

事件

    private void ThrowSword()
    {
        SkillManager._instance._sword.CreateSword();
    }

使用预设体创建剑物体并对剑物体设置

    [SerializeField] private GameObject _swordPrefab;
    [SerializeField] private float _graivty;
    [SerializeField] private Vector2 _launchForce;
    public void CreateSword()
    {
        GameObject newSword = Instantiate(_swordPrefab, _player.transform.position, transform.rotation);
        Sword_Skill_Control swordControl = newSword.GetComponent<Sword_Skill_Control>();
        swordControl.SetupSword(_graivty, _launchForce);
    }

下面代码挂载的剑预设体上 

    public void SetupSword(float graivty, Vector2 launch)
    {
        _rb.gravityScale = graivty;
        _rb.velocity = launch;
    }

16.2.瞄准点设置

1.进入瞄准激活点,退出瞄准非激活点

public class PlayerAimSwordState : PlayerState
{
    public PlayerAimSwordState(Player player, PlayerStateMachine playerStateMachine, string animName) : base(player, playerStateMachine, animName)
    {
    }

    //事件创建剑
    public override void Enter()
    {
        base.Enter();
        SkillManager._instance._sword.DotsActive(true);
    }

    public override void Exit()
    {
        base.Exit();
        SkillManager._instance._sword.DotsActive(false);
    }
}

2.start中使用DotsActive创建一批点,Update使用DotsPosition()来计算不同的位置;

  • AimDirection:计算和player的位置;Camera.main.ScreenToWorldPoint()函数计算世界位置:超出游戏画面也能计算位置,在3D可以计算Z位置;
  • DotsPosition:斜抛公式计算点具体位置;
  • DotsActive创建一批点;
public class Sword_Skill : Skill
{
    [SerializeField] private GameObject _swordPrefab;
    [SerializeField] private float _graivty;
    [SerializeField] private Vector2 _launchForce;

    private Vector2 _finalDirection;

    [Header("Dot Info")]
    [SerializeField] private GameObject _dotPrefab;
    [SerializeField] private int _numberDots;
    [SerializeField] private float _spaceBetweenDots;
    [SerializeField] private Transform _parentTransform;
    private GameObject[] _dots;

    protected override void Start()
    {
        base.Start();
        GenerateDots();
    }
    protected override void Update()
    {
        if (Input.GetKey(KeyCode.Mouse1))
            _finalDirection = new Vector2(AimDirection().normalized.x * _launchForce.x, AimDirection().normalized.y * _launchForce.y);
        if(Input.GetKey(KeyCode.Mouse1))
        {
            for (int i = 0; i < _dots.Length; i++)
            {
                _dots[i].transform.position = (Vector2)_player.transform.position+ DotsPosition(i * _spaceBetweenDots);
            }
        }
    }

    public Vector2 AimDirection()
    {
        Vector2 playerPosition =_player.transform.position;
        Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        Vector2 direction = mousePosition - playerPosition;
        return direction;
    }
    public void DotsActive(bool isActive)
    {
        for (int i = 0; i < _dots.Length; i++)
        {
            _dots[i].SetActive(isActive);
        }
    }
    public void GenerateDots()
    {
        _dots = new GameObject[_numberDots];
        for (int i = 0; i < _numberDots; i++)
        {
            _dots[i] = Instantiate(_dotPrefab, _player.transform.position, Quaternion.identity, _parentTransform);
            _dots[i].SetActive(false);
        }
    }
    //斜抛公式  Physics2D.gravity为[0,-9.8];
    public Vector2 DotsPosition(float time)
    {
        Vector2 position = new Vector2(AimDirection().normalized.x * _launchForce.x, 
        AimDirection().normalized.y * _launchForce.y) * time 
        + 0.5f * (Physics2D.gravity * _graivty) * time * time;
        return position;
    }
}

17.黑洞技能

17.1.创建黑洞,添加热键,使用热键添加敌人transform

1.创建一个黑洞,创建一个圆形,将碰撞器设置为是触发,使用Vector2.Lerp前面快速增长后面慢速增长

    [SerializeField] private float _maxSize;
    [SerializeField] private float _growSpeed;
    [SerializeField] private bool _isGrow;

    [SerializeField] private List<Transform> _targets;

    // Update is called once per frame
    void Update()
    {
        if(_isGrow)
        {
            //取插值,例【0,0】,【5,5】,0.5f,返回【2.5,2.5】
            //随着时间范围越来越小,增长的也越来越小
            transform.localScale = Vector2.Lerp(transform.localScale, new Vector2(_maxSize,_maxSize),_growSpeed *Time.deltaTime);
        }
    }

2.创建一个UI预设体方便使用

3.随着黑洞不断扩张,当碰到敌人时,从热键List内获取一个唯一的热键,传递给热键UI预设体脚本,将文本变成对应热键,按下对应热键将敌人的位置添加进List备用;

    public void SetupHotKey(KeyCode myKeyCode, Transform enemy, BlackHole_Skill_Controller blackHoleScript)
    {
        _sr = GetComponent<SpriteRenderer>();
        //获得唯一的热键,并且文本也设置为此热键
        _myText = GetComponentInChildren<TextMeshProUGUI>();

        _myKeyCode = myKeyCode;
        _myText.text = _myKeyCode.ToString();

        _enemy = enemy;
        _blackHoleScript = blackHoleScript;
    }

    // Update is called once per frame
    void Update()
    {
        if(Input.GetKeyUp(_myKeyCode)) 
        {
            _blackHoleScript.AddEnemy(_enemy);
            _sr.color = Color.clear;
            _myText.color =Color.clear;
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.GetComponent<Enemy>() != null)
        {
            collision.GetComponent<Enemy>().FreezeTimeTrue();
            CreateHotKey(collision);
        }
    }

    private void CreateHotKey(Collider2D collision)
    {
        if (_keyCodeList.Count == 0)
        {
            Debug.Log("Not enough keys in hot key list");
            return;
        }
        //实例化热键
        GameObject newHotKey = Instantiate(_hotKeyPre, collision.transform.position + new Vector3(0, 2), Quaternion.identity);

        KeyCode chosenKeyCode = _keyCodeList[Random.Range(1, _keyCodeList.Count)];
        _keyCodeList.Remove(chosenKeyCode);

        BlackHole_HotKey_Controller newHotkeyScript = newHotKey.GetComponentInChildren<BlackHole_HotKey_Controller>();
        newHotkeyScript.SetupHotKey(chosenKeyCode, collision.transform, this);
    }
    public void AddEnemy(Transform enemyTransform)
    {
        _targets.Add(enemyTransform);
    }

17.2.按下R键在敌人任一一侧创建克隆攻击,攻击完毕应销毁黑洞

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R) && _amountOfAttack > 0)
        {
            DestroyHotkey();
            _cloneAttackReleased = true;
        }
        _attackTime -= Time.deltaTime;
        if (_attackTime < 0 && _cloneAttackReleased)
        {
            _attackTime = _attackCoolDown;
            int random = Random.Range(0, _targets.Count);
            //随机放置在对象的两侧
            float xOffset;
            if (Random.Range(1, 100) > 50)
                xOffset = 2;
            else
                xOffset = -2;
            //调用克隆攻击
            SkillManager._instance._clone.CreateClone(_targets[random], new Vector3(xOffset,0));
            _amountOfAttack--;
            if (_amountOfAttack <= 0)
            {
                _cloneAttackReleased = false;
            }
        }
      
        if (_isShrink)
        {
            transform.localScale = Vector2.Lerp(transform.localScale, new Vector2(-1, -1), _shrinkSpeed * Time.deltaTime);
            if (transform.localScale.x < 0)
                Destroy(gameObject);
        }
    }

    private void DestroyHotkey()
    {
        if (_createHotKeyList.Count == 0)
            return;
        else
        {
            for(int i = 0; i < _createHotKeyList.Count; i++)
            {
                Destroy(_createHotKeyList[i]);
            }
        }
    }

17.3.

17.3.1创建玩家黑洞状态,进入状态飞到最高点时透明,结束变为默认;黑洞释放完毕应将玩家状态设置为Air状态正常掉落

    public override void Enter()
    {
        base.Enter();
        _stateTime = _flyTime;
        _defaultGravity = _player._rb.gravityScale;
        _player._rb.gravityScale = 0;
        _skillUsed =false;
    }

    public override void Exit()
    {
        base.Exit();
        _player._rb.gravityScale = _defaultGravity;
        _player.Transparent(false);

    }

    public override void Update()
    {
        base.Update();
        _stateTime -= Time.deltaTime;
        if (_stateTime > 0)
        {
            _player.SetRigidbobyVolecity(0, _flySpeed);
        }
        else
        {
            _player.SetRigidbobyVolecity(0, -0.1f);
            if(!_skillUsed) 
            {
                //飞到最高点变透明
                _player.Transparent(true);
                SkillManager._instance._blackHole.CanUseSkill();
                _skillUsed = true;
            }
            //黑洞攻击结束
            if (SkillManager._instance._blackHole._blackHoleScript.CanChangeAir())
                _playerStateMachine.ChangeState(_player._airState);
        }
    }
}

17.3.2.黑洞技能脚本设置黑洞脚本的值

    [SerializeField] private float _maxSize;
    [SerializeField] private float _growSpeed;
    [SerializeField] private float _shrinkSpeed;
    [SerializeField] private float _attackCoolDown = 0.3f;
    [SerializeField] private int _amountOfAttack = 4;

    [SerializeField] private float _blackHoleDuration;
    [SerializeField] private GameObject _blackHole;

    public BlackHole_Skill_Controller _blackHoleScript { get; private set; }
    public override bool CanUseSkill()
    {
        return base.CanUseSkill();
    }

    protected override void UseSkill()
    {
        base.UseSkill();
        GameObject blackHole = Instantiate(_blackHole, _player.transform.position, Quaternion.identity);
        _blackHoleScript = blackHole.GetComponent<BlackHole_Skill_Controller>();
        _blackHoleScript.SetupBlackHole(_maxSize, true, false,_growSpeed, _shrinkSpeed, _attackCoolDown, _amountOfAttack, _blackHoleDuration);
    }
}

17.3.3.设置黑洞持续时间,时间耗尽如果有对象可攻击则进入攻击,没有则结束黑洞;黑洞结束则可以切换玩家至air状态

  void Update()
  {
      CLoneAttackLogic();
  }

  private void CLoneAttackLogic()
  {
      if (Input.GetKeyDown(KeyCode.R) && _amountOfAttack > 0)
      {
          DestroyHotkey();
          _cloneAttackReleased = true;
      }
      _attackTime -= Time.deltaTime;
      _blackHoleDuration -= Time.deltaTime;
      //黑洞持续时间耗尽,有可攻击对象攻击,没有退出
      if (_blackHoleDuration < 0)
      {
          _blackHoleDuration = Mathf.Infinity;
          DestroyHotkey();
          if (_targets.Count > 0)
          {
              _cloneAttackReleased = true;
              ReleaseAttack();
          }
          else
              FinishBlackHoleAbility();
      }
      ReleaseAttack();
  }

  private void ReleaseAttack()
  {
      if (_attackTime < 0 && _cloneAttackReleased )
      {
          if (_targets.Count > 0 && _amountOfAttack > 0)
          {
              _attackTime = _attackCoolDown;
              int random = Random.Range(0, _targets.Count);
              //随机放置在对象的两侧
              float xOffset;
              if (Random.Range(1, 100) > 50)
                  xOffset = 2;
              else
                  xOffset = -2;
              //调用克隆攻击
              SkillManager._instance._clone.CreateClone(_targets[random], new Vector3(xOffset, 0));
              _amountOfAttack--;
          }
          //攻击次数耗尽或者没有可攻击目标
          FinishBlackHoleAbility();
      }
  }
  public bool CanChangeAir()
  {
      return _canChangeAirState;
  }
  private void FinishBlackHoleAbility()
  {
      if (_targets.Count == 0 || _amountOfAttack == 0)
      {
          _cloneAttackReleased = false;
          _isShrink = true;
          //从黑洞状态切换到空中状态
          _canChangeAirState = true;
      }
  }

18.水晶技能

18.1.返回记录点

  • 创建一个水晶的旋转和销毁动画
  • 逻辑:如果没有水晶,在当前位置创建一个水晶,如果有则销毁水晶,交换位置后水晶销毁;耗尽水晶时间也没有使用,销毁水晶;
  • 时间消耗殆尽或在已有水晶的情况下再次释放,切换到爆炸动画,此动画有爆炸伤害和销毁事件
  • 设置移动模式,不在可以传送,向最近敌人移动,移动到一定距离发生爆炸
    void Start()
    {
        
    }
    void Update()
    {
        _cristalDuration -= Time.deltaTime;

        if(_canMove && _closestEnemy)
        {
            transform.position = Vector2.MoveTowards(transform.position, _closestEnemy.position, _moveSpeed * Time.deltaTime);
            //如果距离小于1则爆炸
            if (Vector2.Distance(transform.position, _closestEnemy.position) < 1)
            {
                _canMove = false;
                FinishCristal();
            }
        }
        if (_cristalDuration < 0)
        {
            FinishCristal();
        }
        if(_canGrow)
            transform.localScale = Vector2.Lerp(transform.localScale, new Vector2(_maxSize, _maxSize), _growSpeed * Time.deltaTime);
            
    }

    public void FinishCristal()
    {
        if (_canExplode)
        {
            _anim.SetBool("Explode", true);
            _canGrow = true;
        }
        else
        {
            SelfDestroy();
        }
    }

    private void AnimtionExplodeFinish()
    {
        Collider2D[] collider2Ds = Physics2D.OverlapCircleAll(transform.position, _ccd.radius);
        foreach (var hit in collider2Ds)
        {
            if (hit.GetComponent<Enemy>() != null)
                hit.GetComponent<Enemy>().Damage();
        }
    }

    public void SetupCristal(float cristalDuration, bool canExpolode, bool canMove, float moveSpeed, Transform newTransform)
    {
        _cristalDuration = cristalDuration;
        _canExplode = canExpolode;
        _canMove = canMove;
        _moveSpeed = moveSpeed; 
        _closestEnemy = newTransform;
    }

    private void SelfDestroy()
    {
        Destroy(gameObject);
    }
}
    protected override void UseSkill()
    {
        base.UseSkill();
        if (_currentCristal == null)
        {
            _currentCristal = Instantiate(_cristalPrefab, _player.transform.position, Quaternion.identity);
            Cristal_Skill_Controller cristalScript = _currentCristal.GetComponent<Cristal_Skill_Controller>();
            cristalScript.SetupCristal(_cristalDuration, _canExplode, _canMove, _moveSpeed, FindClosestEnemy(cristalScript.transform));
        }
        else//已有水晶,交换位置
        {
            if (_canMove)
                return;
            Vector2 playerPosition = _player.transform.position;
            _player.transform.position = _currentCristal.transform.position;
            _currentCristal.transform.position = playerPosition;

            _currentCristal.GetComponent<Cristal_Skill_Controller>()?.FinishCristal();
        }
    }
}

18.2.检测最近敌人,写在技能基类,派生类继承

    protected Transform FindClosestEnemy(Transform chooseTransform)
    {
        //选择最近敌人,并且面向他
        float minDistance = float.MaxValue;
        Collider2D minDIsCollider = null;
        Collider2D[] colliders = Physics2D.OverlapCircleAll(chooseTransform.position, 25);
        foreach (var collider in colliders)
        {
            if(collider.GetComponent<Enemy>() != null)
            {
                float current = Vector2.Distance(chooseTransform.position, collider.transform.position);
                if (current < minDistance)
                {
                    minDistance = current;
                    minDIsCollider = collider;
                }
            }
        }
        return minDIsCollider.transform;
    }

18.3.水晶攻击模式,有x使用次数,耗尽进入冷却补充,在一定时间内如果没有使用完也会进入冷却补充

    [Header("Multi Stack Cristal")]
    [SerializeField] private bool _canMultiStack;
    [SerializeField] private int _amountOfCrystal;
    [SerializeField] private float _refillCoolDown;
    [SerializeField] private float _useCoolDownWindow;
    [SerializeField] private List<GameObject> _cryst    protected override void UseSkill()
    {
        base.UseSkill();
        if (CanUsemultiStack())
            return;
        else
            _coolDown = 0;
    }

    private bool CanUsemultiStack()
    {

        if (_canMultiStack)
        {
            //使用第一个后,在一定时间内如果没有耗尽crystal,将自动填装
            if (_crystalList.Count > 0)
            {
                if (_crystalList.Count == _amountOfCrystal)
                    Invoke("ResetAbility", _useCoolDownWindow);

                _coolDown = 0;//使用将冷却设为0,因为填装将冷却设置了
                GameObject tailValue = _crystalList[_crystalList.Count - 1];

                GameObject newGameobeject = Instantiate(tailValue, _player.transform.position, Quaternion.identity);
                _crystalList.Remove(tailValue);

                newGameobeject.GetComponent<Crystal_Skill_Controller>().
                    SetupCristal(_crystalDuration, _canExplode, _canMove, _moveSpeed, FindClosestEnemy(newGameobeject.transform));

                //次数耗尽,填充次数并进入冷却
                if (_crystalList.Count == 0)
                {
                    _coolDown = _refillCoolDown;
                    RefillCrystal();
                }
            }
            return true;
        }
        else
            return false;
    }

    private void RefillCrystal()
    {
        int amount = _amountOfCrystal - _crystalList.Count;
        for (int i = 0; i < amount; i++)
        {
            _crystalList.Add(_crystalPrefab);
        }
    }
    private void ResetAbility()
    {
        if (_coolDownTime > 0)
            return;
        _coolDownTime = _refillCoolDown;
        RefillCrystal();
    }alList = new List<GameObject>();

19.克隆攻击产生克隆攻击(概率)

  • 在有敌人目标是克隆攻击,再判断是否会再次产生克隆攻击;
    private void AttackTrigger()
    {
        Collider2D[] collider2Ds = Physics2D.OverlapCircleAll(_attackCheck.position, _attackCheckRadius);
        foreach (var hit in collider2Ds)
        {
            if (hit.GetComponent<Enemy>() != null)
            {
                hit.GetComponent<Enemy>().Damage();
                //克隆攻击概率产生克隆攻击
                if(_canDuplicateClone && _canCreateDuplicate)
                {
                    _canCreateDuplicate = false;
                    if(Random.Range(1, 100) <= _DuplicateChance)
                    {
                        SkillManager._instance._clone.CreateClone(hit.transform, new Vector3(1f * _facingDirection, 0, 0));
                    }
                }
            }
        }
    }

20.角色统计

20.1.每个角色和玩家都需要角色统计来计算数值

添加上角色统计组件,在entity获取组件,子类自动继承

public class CharacterStats : MonoBehaviour
{
    public int _damage;

    public int _maxHealth;
    private int _health;

    //初始血量
    private void Start()
    {
        _health = _maxHealth;
    }
    
    //造成伤害
    public void Damage(int damage)
    {
        _health -= damage;
    }
}

20.2.玩家和敌人各自重写角色统计函数

  • DoDamage:造成伤害目标位敌人;
  • takeDamage: 承受伤害目标为自己
public class PlayerStats : CharacterStats
{
    private Player _player;
    //造成伤害目标位敌人
    public override void DoDamage(CharacterStats target)
    {
        base.DoDamage(target);
    }

    //承受伤害目标为自己
    public override void TakeDamage(int damage)
    {
        base.TakeDamage(damage);
        _player.DamageEffect();
    }

    protected override void Die()
    {
        base.Die();
    }

    protected override void Start()
    {
        base.Start();
        _player = GetComponent<Player>();
    }
}
public class EnemyStats : CharacterStats
{
    private Skeleton_Enemy _enemy;
    public override void DoDamage(CharacterStats target)
    {
        base.DoDamage(target);
    }

    public override void TakeDamage(int damage)
    {
        base.TakeDamage(damage);
    }

    protected override void Die()
    {
        base.Die();
    }

    protected override void Start()
    {
        base.Start();
        _enemy.DamageEffect();
    }
}

20.3.添加主要数值,伤害计算闪避和护甲值

    [Header("Major Stats")]
    public Stat _strength;//力量:1点增加伤害1点和制造能力1%
    public Stat _agility;//敏捷:1点增加闪避和制造速度1%
    public Stat _intelligence;//智力:1点增加魔法伤害1点和法抗1%
    public Stat _vitality;//活力:1点增加生命值3点

    [Header("Defensive Stat")]
    public Stat _maxHealth;//最高血量
    public Stat _armor;//护甲
    public Stat _evasion;//闪避


    public Stat _damage;
    [SerializeField] protected int _currentHealth;

    //造成伤害,参数是收到伤害的对象
    public virtual void DoDamage(CharacterStats target)
    {
        //计算闪避值,并判断是否闪避成功
        if (TargetCanAvoidAttack(target))
            return;
        //计算总伤害,减去护甲值
        int totalDamage = CheckTargetArmor(target);

        target.TakeDamage(totalDamage);
    }

    private int CheckTargetArmor(CharacterStats target)
    {
        int totalDamage = _damage.GetBaseValue() + _strength.GetBaseValue();
        totalDamage -= target._armor.GetBaseValue();//
        totalDamage = Mathf.Clamp(totalDamage, 0, int.MaxValue);//value在两者之间返回value,小于最小值返回最小值,大于最大返回最大
        return totalDamage;
    }

    private bool TargetCanAvoidAttack(CharacterStats target)
    {
        int totalEvasion = target._evasion.GetBaseValue() + target._agility.GetBaseValue();
        if (Random.Range(1, 100) <= totalEvasion)
        {
            Debug.Log("ATTACK EVASION");
            return true;
        }
        return false;
    }

20.4.暴击倍率和暴击概率

  • 暴击倍率 = 初始暴击倍率 + 1点力量加1点暴击倍率
  • 暴击概率 = 初始暴击概率 + 1点敏捷加移动暴击概率
    private bool CanCrit()
    {
        int critChance = _critChance.GetBaseValue() + _agility.GetBaseValue();
        if(Random.Range(1, 100) <= critChance)
            return true;   
        return false;
    }

    private int CheckTargetArmor(CharacterStats target)
    {
        int totalDamage = _damage.GetBaseValue() + _strength.GetBaseValue();
        totalDamage -= target._armor.GetBaseValue();//
        totalDamage = Mathf.Clamp(totalDamage, 0, int.MaxValue);//value在两者之间返回value,小于最小值返回最小值,大于最大返回最大
        return totalDamage;
    }

20.5.计算魔法伤害

  • 魔法伤害 = (1点智力加1点魔法伤害 + 火魔法伤害 + 冰魔法伤害 + 光魔法伤害 )- (魔法抗性 + 1点智力加3点魔法抗性);
    private void ApplyAilments(bool isIgnite, bool isChilled, bool isShocked)
    {
        if (_isIgnite || _isChilled || _isShocked)
            return;
        _isIgnite = isIgnite;
        _isChilled = isChilled; 
        _isShocked = isShocked;
    }

    private int CalculateCritDamage(int totalDamage)
    {
        float totalCritPower = (_critPower.GetBaseValue() + _strength.GetBaseValue()) * 0.01f;
        float critDamage = totalDamage * totalCritPower;

        return Mathf.RoundToInt(critDamage);
    }

20.6.根据最高的属性魔法给目标添加上状态

    protected virtual void SelectElement( CharacterStats target, int fireDamage, int iceDamage, int lightningDamage)
    {
        //3种元素伤害都不超过0,则不造成特殊效果
        if (Mathf.Max(fireDamage, iceDamage, lightningDamage) <= 0)
            return;

        //选择最大元素,决定受到的特殊效果
        bool canApplyIgnited = fireDamage > iceDamage && fireDamage > lightningDamage;
        bool canApplyChilled = iceDamage > fireDamage && iceDamage > lightningDamage;
        bool canApplyShocked = lightningDamage > fireDamage && lightningDamage > iceDamage;

        //如果最大元素伤害相同,随机一个
        while (!canApplyIgnited && !canApplyChilled && !canApplyShocked)
        {
            if (fireDamage > 0 && Random.value < 0.33f)
            {
                canApplyIgnited = true;
                break;
            }
            if (iceDamage > 0 && Random.value < 0.5f)
            {
                canApplyChilled = true;
                break;
            }
            if (lightningDamage > 0 && Random.value < 1f)
            {
                canApplyChilled = true;
                break;
            }
        }
        //设置火焰伤害
        target.SetupIgniteDamage(Mathf.RoundToInt(fireDamage * 0.2f));

        target.ApplyAilments(canApplyIgnited, canApplyChilled, canApplyShocked);
    }
    private void ApplyAilments(bool isIgnite, bool isChilled, bool isShocked)
    {
        if (_isIgnite || _isChilled || _isShocked)
            return;


        if (isIgnite)
        {
            _isIgnite = isIgnite;
            _igniteTimer = 2;
            //Debug.Log("_isIgnite");
        }
        if (isChilled)
        {
            _isChilled = isChilled;
            _chilledTimer = 2;
            //Debug.Log("_isChilled");
        }
        if (isShocked)
        {
            _isShocked = isShocked;
            _shockTimer = 2;
            //Debug.Log("_isShocked");
        }
    }

20.6.1.根据不同的状态,添加不同的特殊效果;

    [Header("Magic Stat")]
    public Stat _fireDamage;
    public Stat _iceDamage;
    public Stat _lightningDamage;

    public bool _isIgnite;//点燃,持续造成火焰伤害(自己)
    public bool _isChilled;//冰冻,减速 + 穿甲 //处于冰冻状态减少20%的护甲(目标)
    public bool _isShocked;//眩晕,- 命中率  //处于震撼状态减少命中率,及增加目标的闪避值(自己)

    private float _igniteTimer;//燃烧状态的时间
    private float _chilledTimer;
    private float _shockTimer;

    private float _igniteDamageCoolDown = 0.3f;//燃烧伤害的冷却
    private float _igniteDamageTimer;//燃烧伤害的更新
    private int _igniteDamage;
    protected virtual void Update()
    {
        _igniteTimer -= Time.deltaTime;
        _chilledTimer -= Time.deltaTime;
        _shockTimer -= Time.deltaTime;

        _igniteDamageTimer -= Time.deltaTime;//燃烧间隔

        if (_igniteTimer < 0)
            _isIgnite = false;
        if(_chilledTimer < 0)
            _isChilled = false;
        if(_shockTimer < 0)
            _isShocked = false;

        if(_isIgnite == true && _igniteDamageTimer < 0)
        {
            _currentHealth -= _igniteDamage;
            _igniteDamageTimer = _igniteDamageCoolDown;
            Debug.Log("Ignite Damage" + _igniteDamage);
        }
    }

20.7.不同状态不同的FX视觉效果

  • 采用两种颜色切换来做效果

    [Header("Ailment Color")]
    public Color[] _ignitedColor;
    public Color[] _chilledColor;
    public Color[] _shockColor;

    private void CancelColorChange()
    {
        CancelInvoke();
        sr.color = Color.white;
    }
    public void IgniteFxFor(float s)
    {
        InvokeRepeating("IgniteColorFX", 0, 0.3f);
        Invoke("CancelColorChange", s);
    }
    public void ChilledFXFor(float s)
    {
        InvokeRepeating("ChilledColorFX", 0, 0.3f);
        Invoke("CancelColorChange", s);
    }
    public void ShockFXFor(float s)
    {
        InvokeRepeating("ShockColorFX", 0, 0.3f);
        Invoke("CancelColorChange", s);
    }
    private void IgniteColorFX()
    {
        if(sr.color != _ignitedColor[0])
            sr.color= _ignitedColor[0];
        else
            sr.color = _ignitedColor[1];
    }

    private void ChilledColorFX()
    {
        if (sr.color != _chilledColor[0])
            sr.color = _chilledColor[0];
        else
            sr.color = _chilledColor[1];
    }
    private void ShockColorFX()
    {
        if (sr.color != _shockColor[0])
            sr.color = _shockColor[0];
        else
            sr.color = _shockColor[1];

20.8.减速效果

  • 记录速度默认值
  • 减少动画速度和移动速度等,%的速度
    public override void SlowEntityBy(float slowPercentage, float slowDuration)
    {
        _moveSpeed = _moveSpeed * (1 - slowPercentage);
        _jumpForce = _jumpForce * (1 - slowPercentage);
        _dashSpeed = _dashSpeed * (1 - slowPercentage);
        _anim.speed = _anim.speed * (1 - slowPercentage);

        Invoke("ReturnDefaultSpeed", slowDuration);
    }

    public override void ReturnDefaultSpeed()
    {
        base.ReturnDefaultSpeed();
        _moveSpeed = _moveDefaultSpeed;
        _jumpForce = _jumpDefaultForce;
        _dashSpeed = _dashDefaultSpeed;
    }

20.9.雷击

  • 如果已处于震撼状态,再被施加震撼状态,将会在此敌人对最近敌人(没有则此敌人)发射雷击;
public class ThunderStrike_Contorller : MonoBehaviour
{
    private CharacterStats _targetStats;
    private float _speed;
    private int _damage;

    private bool _triggered;
    private Animator _anim;
    void Start()
    {
        _anim = GetComponentInChildren<Animator>();
    }

    public void SetupThunderStrike(CharacterStats targetStats, int damage, float speed)
    {
        _targetStats = targetStats;
        _damage = damage;
        _speed = speed;
    }
    void Update()
    {
        if (_targetStats == null)
            return;
        //已触发雷击效果,不在触发
        if (_triggered)
            return;
        //向目标靠近
        transform.position = Vector2.MoveTowards(transform.position, _targetStats.transform.position, _speed * Time.deltaTime);
        transform.right = transform.position - _targetStats.transform.position;

        //雷电和目标距离小于0.1,切换攻击动画
        if(Vector2.Distance(transform.position, _targetStats.transform.position) < 0.1f)
        {
            //打击动画变大
            _anim.transform.localRotation = Quaternion.identity;
            _anim.transform.localPosition = new Vector3(0, 0.5f);//与父物体的相对位置,使雷击和地面有一定距离
            transform.localRotation = Quaternion.identity;
            transform.localScale = new Vector3(3, 3);


            //造成伤害和切换动画
            Invoke("DamageAndSelfDestroy", 0.2f);//让动画先播一会,效果更好
            _anim.SetBool("Hit", true);
            _triggered = true;
        }
    }

    private void DamageAndSelfDestroy()
    {
        _targetStats.ApplyShock(true);
        _targetStats.TakeDamage(1);
        Destroy(gameObject, 0.4f);
    }
}
        if (isShocked && canApplyShock)
        {
            if (_isShocked == false)//没有震撼状态
            {
                ApplyShock(isShocked);
            }
            else//已有震撼状态
            {
                //玩家不受此效果
                if (GetComponent<Player>() != null)
                    return;

                HitNearesTagetWithShockStrike();
            }
        }
    private void HitNearesTagetWithShockStrike()
    {
        float minDistance = float.MaxValue;
        Transform closestEnemy = null;
        Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 25);
        foreach (var collider in colliders)
        {
            //不选择自己
            if (collider.GetComponent<Enemy>() != null && Vector2.Distance(collider.transform.position, transform.position) > 0.1f)
            {
                float current = Vector2.Distance(transform.position, collider.transform.position);
                if (current < minDistance)
                {
                    minDistance = current;
                    closestEnemy = collider.transform;
                }
            }
        }
        if (closestEnemy == null)
            closestEnemy = transform;

        GameObject newThunderstrike = Instantiate(_thunderStrikePrefab, transform.position, Quaternion.identity);
        ThunderStrike_Contorller newScript = newThunderstrike.GetComponent<ThunderStrike_Contorller>();
        newScript.SetupThunderStrike(closestEnemy.GetComponent<CharacterStats>(), _shockDamage, _ThunderStrickSpeed);
    }

21.装备背包栏

21.1.ScriptableObject类

  • 继承ScriptableObject类,类内写物品信息
public class ItemData : ScriptableObject //继承ScriptableObject类
{
    //物品信息
    public string _itemName;
    public Sprite _Icon;

}
  • 创建资产菜单,才可以创建物品 
[CreateAssetMenu(fileName = "New Item Menu", menuName = "Item/Date")]

 

  • 如何使用使用物品 
public class ItemObject : MonoBehaviour
{
    [SerializeField] private ItemData _itemData;//获取物品数据
    private SpriteRenderer _sr;
    void Start()
    {
        _sr = GetComponent<SpriteRenderer>();
        _sr.sprite = _itemData._icon;
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.GetComponent<Player>() != null)
        {
            Debug.Log("Pick up item" + _itemData._itemName);
            Destroy(gameObject);
        }
    }
}

21.2.对物品添加和对物品的包装

  • 对物品的包装,添加堆叠数量 
[Serializable]
public class InventoryItem
{
    private ItemData _itemData;
    public int _stackSize;

    //构造函数,对ItemData的包装
    public InventoryItem(ItemData itemData)
    {
        _itemData = itemData;
        _stackSize = 1;
    }

    public void AddStack() => _stackSize++;
    public void RemoveStack() => _stackSize--;
}
  • 添加物品添加进链表,字典树查找物品的堆叠数量; 
public class Inventory : MonoBehaviour
{
    public static Inventory _Instance;

    public List<InventoryItem> _inventoryItems;

    public Dictionary<ItemData, InventoryItem> _inventoryDictionary;

    private void Awake()
    {
        if (_Instance == null)
            _Instance = this;
        else
            Destroy(this);
    }
    private void Start()
    {
        _inventoryItems = new List<InventoryItem>();
        _inventoryDictionary = new Dictionary<ItemData, InventoryItem>();
    }

    public void AddItem(ItemData item)
    {
        if (_inventoryDictionary.TryGetValue(item, out InventoryItem value))
        {
            value.AddStack();
        }
        else//还没有此物品,添加新物品
        {
            InventoryItem newItem = new InventoryItem(item);
            _inventoryItems.Add(newItem);
            _inventoryDictionary[item] = newItem;
        }
    }
    public void RemoveItem(ItemData item)
    {
        if (_inventoryDictionary.TryGetValue(item, out InventoryItem value))
        {
            if (value._stackSize <= 1)
            {
                _inventoryItems.Remove(value);
                _inventoryDictionary.Remove(item);
            }
            else
                value.RemoveStack();
        }
    }
}

21.3.物品栏的制作

  •  先创建画布和图像和文本,设置为屏幕大小缩放和分辨率为1920和1080;

  • 每当拾取和丢弃物品时,更新物品栏;
public class UI_ItemStack : MonoBehaviour
{
    [SerializeField] private Image _image;
    [SerializeField] private TextMeshProUGUI _itemText;

    public InventoryItem _item;

    public void UpdateSlot(InventoryItem itemData)
    {
        GetComponent<Image>().color = Color.white;//有物品不在透明

        _item = itemData;
        if (_item != null)
        {
            if (_item._stackSize > 1)
            {
                _image.sprite = _item._itemData._icon;
                _itemText.text = _item._stackSize.ToString();
            }
            else
            {
                _image.sprite = _item._itemData._icon;
                _itemText.text = "";
            }
        }
    }
}

20.4.将物品分为材料和装备,使用enum分别,武器(继承物品类)有分为武器、铠甲、护身符、携带物,也使用enum分别;

//物品类
public enum Itemtype
{
    Material,
    Equipment
}

[CreateAssetMenu(fileName = "New Item Menu", menuName = "Data/Item")]
public class ItemData : ScriptableObject //继承ScriptableObject类
{
    //物品信息
    public string _itemName;
    public Sprite _icon;
    public Itemtype _itemtype;
}

//装备类继承物品类
public enum Equipment
{
    Weapon, //武器
    Armor,  //盔甲
    Amulet, //护身符
    Flask   //携带物
}

[CreateAssetMenu(fileName = "New Item Menu", menuName = "Data/Equipment")]
public class ItemDataEquipment : ItemData
{
    public Equipment _quipmentType;
}

添加或删除物品后更新物品栏 

    //更新物品栏
    private void UpdateSlotUI()
    {
        for(int i = 0; i < _inventory.Count; i++)
        {
            _inventoryItemSlots[i].UpdateSlot(_inventory[i]);
        }
        for(int i = 0; i < _stash.Count; i++)
        {
            _stashItemSlots[i].UpdateSlot(_stash[i]);
        }
    }

    //已有此物品添加数量,没有添加物品
    public void AddItem(ItemData item)
    {
        if (item._itemtype == Itemtype.Material)
            AddToInventory(item);
        else if (item._itemtype == Itemtype.Equipment)
            AddToStash(item);

        UpdateSlotUI();
    }

    //添加装备至装备栏
    private void AddToStash(ItemData item)
    {
        if (_stashDictionary.TryGetValue(item, out InventoryItem stashValue))
        {
            stashValue.AddStack();
        }
        else
        {
            InventoryItem newItem = new InventoryItem(item);
            _stash.Add(newItem);
            _stashDictionary[item] = newItem;
        }
    }

    //添加材料至材料栏
    private void AddToInventory(ItemData item)
    {
        if (_inventoryDictionary.TryGetValue(item, out InventoryItem value))
        {
            value.AddStack();
        }
        else//还没有此物品,添加新物品
        {
            InventoryItem newItem = new InventoryItem(item);
            _inventory.Add(newItem);
            _inventoryDictionary[item] = newItem;
        }
    }

20.5.装卸装备

1.继承IPointerDownHandler接口类,重写OnPointerDown类,点击装备栏就会调用此函数

public class UI_ItemStack : MonoBehaviour , IPointerDownHandler
{
    [SerializeField] private Image _image;
    [SerializeField] private TextMeshProUGUI _itemText;

    public InventoryItem _item;

    //鼠标点击的物体,触发此事件
    public void OnPointerDown(PointerEventData eventData)
    {

        if (_item._itemData._itemtype == Itemtype.Equipment)
        {
            Inventory._Instance.EquipItem(_item._itemData);
        }
    }
}

2.装备装备,每种装备类型只能装备一种,有旧装备放回装备储存区,然后装备新装备 

    //装备装备,在装备栏显示
    //没有装备此类型,直接装备;已装备此类型,去除当前装备再装备新装备
    public void EquipItem(ItemData itemData)
    {
        //转化为ItemDataEquipment,获取装备类型来达到比较的目的
        ItemDataEquipment newEquipment = itemData as ItemDataEquipment;

        ItemDataEquipment OldEquipment = null;
        //获取键值对,来比较装备类型
        foreach (KeyValuePair<ItemDataEquipment, InventoryItem> item in _equipmentDictionary)
        {
            if (newEquipment._equipmentType == item.Key._equipmentType)
                OldEquipment = item.Key;
        }

        //卸去旧装备,放回装备储藏区
        if (OldEquipment != null)
        {
            ItemToRemove(OldEquipment);
            AddItem(OldEquipment);
        }

        //装备新装备
        InventoryItem newInventory = new InventoryItem(newEquipment);
        _equipment.Add(newInventory);
        _equipmentDictionary[newEquipment] = newInventory;

        RemoveItem(newEquipment);//装备储存栏去除物品
    }

3.重写此函数,点击卸去装备移除属性

public class UI_EquipmentSlot : UI_ItemStack
{
    public EquipmentType _equipmentType;//装备类型
    
    //点击装备栏物品卸下物品,减去装备属性
    public override void OnPointerDown(PointerEventData eventData)
    {
        if (_image == null)
            return; 
        ItemDataEquipment current = _item._itemData as ItemDataEquipment;

        Inventory._Instance.Unequipment(current);
        Inventory._Instance.AddItem(current);
        CleanUpSlot();
    }
}

20.6.获得装备属性和敌人随难度提升属性

1.装备类添加属性,每当调用装备函数后就修改值;

[CreateAssetMenu(fileName = "New Item Menu", menuName = "Data/Equipment")]
public class ItemDataEquipment : ItemData
{
    public EquipmentType _equipmentType;
    //装备的属性,用于修改玩家属性
    [Header("Major Stats")]
    public int _strength;//力量:1点增加伤害1点和暴击倍率1%
    public int _agility;//敏捷:1点增加闪避和暴击概率1%
    public int _intelligence;//智力:1点增加魔法伤害1点和法抗3点
    public int _vitality;//活力:1点增加生命值5点

    [Header("Offensive Stats")]
    public int _damage;//基础伤害
    public int _critPower;//暴击倍率
    public int _critChance;//暴击概率


    [Header("Defensive Stats")]
    public int _maxHealth;//最高血量
    public int _armor;//护甲
    public int _evasion;//闪避
    public int _magicResistance;//魔抗

    [Header("Magic Stats")]
    public int _fireDamage;
    public int _iceDamage;
    public int _lightningDamage;


    //装备装备
    public void AddModifliers()
    {
        //获取玩家
        Player player = PlayerManager._instance._player;

        //加上属性
        player._stats._strength.AddModifiers(_strength);
        player._stats._agility.AddModifiers(_agility);
        player._stats._intelligence.AddModifiers(_intelligence);
        player._stats._vitality.AddModifiers(_vitality);

        player._stats._damage.AddModifiers(_damage);
        player._stats._critChance.AddModifiers(_critChance);
        player._stats._critPower.AddModifiers(_critPower);

        player._stats._maxHealth.AddModifiers(_maxHealth);
        player._stats._armor.AddModifiers(_armor);
        player._stats._evasion.AddModifiers(_evasion);
        player._stats._magicResistance.AddModifiers(_magicResistance);

        player._stats._fireDamage.AddModifiers(_fireDamage);
        player._stats._iceDamage.AddModifiers(_iceDamage);
        player._stats._lightningDamage.AddModifiers(_lightningDamage);

    }
    public void RemoveModifliers()
    {
        //获取玩家
        Player player = PlayerManager._instance._player;

        //减上属性
        player._stats._strength.RemoveModifiers(_strength);
        player._stats._agility.RemoveModifiers(_agility);
        player._stats._intelligence.RemoveModifiers(_intelligence);
        player._stats._vitality.RemoveModifiers(_vitality);

        player._stats._damage.RemoveModifiers(_damage);
        player._stats._critChance.RemoveModifiers(_critChance);
        player._stats._critPower.RemoveModifiers(_critPower);

        player._stats._maxHealth.RemoveModifiers(_maxHealth);
        player._stats._armor.RemoveModifiers(_armor);
        player._stats._evasion.RemoveModifiers(_evasion);
        player._stats._magicResistance.RemoveModifiers(_magicResistance);

        player._stats._fireDamage.RemoveModifiers(_fireDamage);
        player._stats._iceDamage.RemoveModifiers(_iceDamage);
        player._stats._lightningDamage.RemoveModifiers(_lightningDamage);
    }
}

2.敌人随着难度提升属性

    [Header("Level Details")]
    public int _level = 1;//难度等级

    [Range(0f, 1.0f)]
    public float _percentageModifiers = .4f ;//每提升一级难度属性提升此%

    protected override void Start()
    {
        ApplyLevelModifiers();
        base.Start();
        _enemy = GetComponent<Skeleton_Enemy>();
    }

    private void ApplyLevelModifiers()
    {
        Modify(_strength);
        Modify(_agility);
        Modify(_intelligence);
        Modify(_vitality);

        Modify(_damage);
        Modify(_critPower);
        Modify(_critChance);

        Modify(_maxHealth);
        Modify(_armor);
        Modify(_evasion);
        Modify(_magicResistance);

        Modify(_fireDamage);
        Modify(_iceDamage);
        Modify(_lightningDamage);
    }

    public virtual void Modify(Stat stat)
    {
        for(int i = 0; i < _level; i++)
        {
            float modifier = stat.GetFinishValue() * _percentageModifiers;
            stat.AddModifiers(Mathf.RoundToInt(modifier));
        }
    }

20.7.制作物品

1.武器数据类添加一个制作要求列表

public class ItemDataEquipment : ItemData
{
    [Header("Craft Requirements")]
    public List<InventoryItem> _craftMaterials;//所需材料表
}

制造UI添加此武器数据,点击制作UIslot,将会制作

public class UI_CraftSlot : UI_ItemStack
{
    private void OnEnable()
    {
        UpdateSlot(_item);
    }
    public override void OnPointerDown(PointerEventData eventData)
    {
        ItemDataEquipment itemToCraft = _item._itemData as ItemDataEquipment;
        //点击则会制造物品
        if(Inventory._Instance.CanCraft(itemToCraft, itemToCraft._craftMaterials))
        {
            //制作成功可以播放成功欢快的音乐;
        }
    }

}

2.比较仓库材料和制作所需材料是否足够,满足至制作

    //制造装备
    public bool CanCraft(ItemDataEquipment itemOfCraft, List<InventoryItem> craftRequirements)
    {
        List<InventoryItem> materialToRemove = new List<InventoryItem>();//记录制作所需的材料

        //比较是否有足够的材料
        for(int i = 0; i < craftRequirements.Count; i++)
        {
            if (_inventoryDictionary.TryGetValue(craftRequirements[i]._itemData, out InventoryItem inventoryValue))
            {
                if (craftRequirements[i]._stackSize <= inventoryValue._stackSize)
                {
                    materialToRemove.Add(craftRequirements[i]);
                }
                else
                {
                    Debug.Log("not enough material");
                    return false;
                }
            }
            else
            {
                Debug.Log("not enough material");
                return false;
            }
        }

        //移除制作所需材料
        for(int i = 0; i < materialToRemove.Count; i++)
        {
            for(int j = 0; j < materialToRemove[i]._stackSize; j++)
            {
                RemoveItem(materialToRemove[i]._itemData);
            }
        }

20.8.敌人死亡掉落物品

1.因为爆出物品,物品会向左右射出需要使用碰撞器和刚体,所以让子类来做物品拾捡检测

public class ItemTrigger : MonoBehaviour
{
    private ItemObject _itemObject => GetComponentInParent<ItemObject>();
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.GetComponent<Player>() != null)
        {
            _itemObject.PickUpItem();
        }
    }
}
public class ItemObject : MonoBehaviour
{
    private ItemData _itemData;//获取物品数据

    private Rigidbody2D _rb => GetComponent<Rigidbody2D>();

    //爆出物品设置物品种类和弹射速度
    public void SetupItem(ItemData itemData, Vector2 volecity)
    {
        _itemData = itemData;
        _rb.velocity = volecity;
        OnValidate();
    }    
    //当被加载和面板被修改将会调用
    private void OnValidate()
    {
        if (_itemData == null)
            return;
        GetComponent<SpriteRenderer>().sprite = _itemData._icon;
        gameObject.name = "item object - " + _itemData.name.ToString();
    }

    //因为爆出物品,物品会向左右射出需要使用碰撞器和刚体,所以让子类来做物品拾捡检测
    public void PickUpItem()
    {
        Debug.Log("Pick up item" + _itemData._itemName);
        Inventory._Instance.AddItem(_itemData);
        Destroy(gameObject);
    }
}

物品掉落类挂载在具体的敌人上, 此脚本有一个掉落物品列表,当敌人死亡调用物品掉落函数;

  • 掉落物品:物品有掉落率,算出是否掉落后;再判断掉落数量:材料类可以掉落多个,装备一次只能掉落一个;
[Serializable]
struct DropChance
{
    public DropChance(int dropChance, ItemData itemData)
    {
        _dropChance = dropChance;
        _itemData = itemData;
    }
    [Range(0, 100)]
    public int _dropChance;

    public ItemData _itemData;
}

public class ItemDrop : MonoBehaviour
{
    [SerializeField] private GameObject _itemObjectPrefab;//物品对象

    //可能掉落物品列表,为固长
    [SerializeField] private DropChance[] _possibleIDroptem;
    //敌人死亡后改掉落的物品
    private List<ItemData> _dropList = new List<ItemData>();

    public void GeneralDrop()
    {
        for (int i = 0; i < _possibleIDroptem.Length; i++)
        {
            //将物品加入即将掉落列表
            if (UnityEngine.Random.Range(1, 100) <= _possibleIDroptem[i]._dropChance)
                _dropList.Add(_possibleIDroptem[i]._itemData);
        }
        for (int i = 0; i < _dropList.Count; i++)
        {
            //材料可能掉落多个
            if (_dropList[i]._itemtype == Itemtype.Material)
            {
                int j = UnityEngine.Random.Range(1, 3);
                while (j > 0)
                {
                    DropItem(_dropList[i]);
                    j--;
                }
            }
            else
                DropItem(_dropList[i]);
        }
        _dropList.Clear();//最后清理
    }
    public void DropItem(ItemData itemData)
    {
        //此物体挂载在敌人上
        ItemObject newItemObject = Instantiate(_itemObjectPrefab, transform.position, Quaternion.identity).GetComponent<ItemObject>();

        //随机速度
        Vector2 randomVolecity = new Vector2(UnityEngine.Random.Range(-5, 5), UnityEngine.Random.Range(12, 18));
        newItemObject.SetupItem(itemData, randomVolecity);

    }
}

20.9.玩家死亡掉落物品

1.玩家死亡后调用此函数;

public class PlayerItemDrop : ItemDrop
{
    [Header("Player's Drop ")]
    public float _chanceToLoseItems;
    public float _chanceToLoseMaterial;

    public override void GeneralDrop()
    {
        //记录需要删除的元素,不能边遍历边删除
        List<InventoryItem> itemToUnequip = new List<InventoryItem>();
        List<InventoryItem> MaterialToLose = new List<InventoryItem>();

        foreach(InventoryItem item in Inventory._Instance.GetEquipmentList()) 
        {
            //死亡判断是否掉落装备
            if(Random.Range(1, 100) <= _chanceToLoseItems)
            {
                Debug.Log("Item drop" + item._itemData._itemName);
                DropItem(item._itemData);
                itemToUnequip.Add(item);
            }
        }
        //移除装备
        for(int i = 0; i < itemToUnequip.Count; i++)
            Inventory._Instance.Unequipment(itemToUnequip[i]._itemData as ItemDataEquipment);

        foreach(InventoryItem material in Inventory._Instance.GetMaterialList())
        {
            if(Random.Range(1, 100) <= _chanceToLoseMaterial)
            {
                Debug.Log("Item drop" + material._itemData._itemName);
                DropItem(material._itemData);
                MaterialToLose.Add(material);
            }
        }
        for (int i = 0; i < MaterialToLose.Count; i++)
            Inventory._Instance.RemoveItem(MaterialToLose[i]._itemData);

    }
}

2.Ctrl+鼠标左键删除物品,UI_ItemStack类

    //鼠标点击的物体,触发此事件
    public virtual void OnPointerDown(PointerEventData eventData)
    {
        //Ctrl+鼠标左键删除物品
        if(Input.GetKey(KeyCode.LeftControl)) 
        {
            Inventory._Instance.RemoveItem(_item._itemData);
            return;
        }
        if (_item._itemData._itemtype == Itemtype.Equipment)
        {
            Inventory._Instance.EquipItem(_item._itemData);
        }
    }

21.1.装备特效制作

装备特效类

[CreateAssetMenu(fileName = "New Item Data", menuName = "Data/Item effect")]
public class ItemEffect : ScriptableObject
{
    public virtual void ExecuteEffect()
    {
        Debug.Log("Execute Effect");
    }
}

装备类添加装备特效列表

public class ItemDataEquipment : ItemData
{
    //装备特效
    public ItemEffect[] _itemEffects;

    //使用此武器的特效
    public void ItemEffect()
    {
        foreach(var effect in _itemEffects)
        {
            effect.ExecuteEffect();
        }
    }
}

传入装备类型获得当前装备 

    //获取武器特效
    public ItemDataEquipment GetEquipmentEffect(EquipmentType key)
    {
        ItemDataEquipment equipment = null;
        foreach(var item in _equipmentDictionary)
        {
            if(item.Key._equipmentType == key)
            {
                equipment = item.Key;
            }
        }
        return equipment;
    }

普通攻击位置调用装备的特效函数 

//使用武器的攻击特效
Inventory._Instance.GetEquipmentEffect(EquipmentType.Weapon)?.ItemEffect();

21.1.雷击特效 

 继承装备特效,添加预设体,;当被攻击时实例雷击预设体

[CreateAssetMenu(fileName = "Thunder Strike Data", menuName = "Data/Item effect/Thunder strike")]
public class ThunderStrickEffect : ItemEffect
{
    [SerializeField] private GameObject _thunderStrikePrefab;

    public override void ExecuteEffect(Transform enemyTransform)
    {
        GameObject thunderStrike = Instantiate(_thunderStrikePrefab, enemyTransform.position, Quaternion.identity);
        Destroy(thunderStrike, 0.5f);
    }
}

  被雷击预设体碰撞器碰撞,造成伤害

public class ThunderStrikeController : MonoBehaviour
{
    //计算伤害
    private void OnTriggerEnter2D(Collider2D collision)
    {
        //获取统计组件,用于计算
        PlayerStats _playerStat = PlayerManager._instance._player.GetComponent<PlayerStats>();

        if (collision.GetComponent<Enemy>() != null)
        {
            EnemyStats enemyStats = collision.GetComponent<EnemyStats>();
            _playerStat.DoDamage(enemyStats);
        }
    }
}

21.2.冰火特效 

第三次攻击触发        

[CreateAssetMenu(fileName = "IceAndFire Data", menuName = "Data/Item effect/Ice And Fire")]
public class IceAndFireEffect : ItemEffect
{
    public GameObject _iceAndFirePrefab;
    public float _xVelocity;
     
    public override void ExecuteEffect(Transform enemyTransform)
    {
        Player player = PlayerManager._instance._player;
        if(player._attackState.comboCounter == 2 )
        {
            GameObject iceAndFire = Instantiate(_iceAndFirePrefab, player.transform.position, player.transform.rotation);
            //设置速度
            iceAndFire.GetComponent<Rigidbody2D>().velocity = new Vector2(_xVelocity * player.facingDir, 0);
        }
    }
}

21.3护身符特效:技能将造成装备特效

在造成伤害位置,检测是否护身符和此护身符是否有装备特效 

    private void SwordSkillDamage(Enemy enemy)
    {
        _player._stats.DoDamage(enemy.GetComponent<CharacterStats>());

        //造成装备特性
        ItemDataEquipment amulet = Inventory._Instance.GetEquipment(EquipmentType.Amulet);
        if (amulet != null)
        {
            amulet.ItemEffect(enemy.transform);
        }
    }

21.4.回血特效

[CreateAssetMenu(fileName = "Health Effect Data", menuName = "Data/Item effect/Health Effect")]
public class HealthEffect : ItemEffect
{
    [Range(0f, 1f)]
    [SerializeField] private float _healthPercent;

    public override void ExecuteEffect(Transform enemyTransform)
    {
        //获取玩家统计
        PlayerStats playerStats = PlayerManager._instance._player.GetComponent<PlayerStats>();

        //每次增加一定比例的最大血量
        int health = Mathf.RoundToInt(playerStats.GetMaxHealth() * _healthPercent);
        playerStats.IncreaseHealthBy(health);
    }
}

 制作血瓶

    public void UseFlask()
    {
        ItemDataEquipment currentFlask = GetEquipment(EquipmentType.Flask);
        //已装备瓶子
        if (currentFlask != null)
        {
            //不在冷却
            if ( Time.deltaTime > _flaskLaskCoolDown + currentFlask._coolDown)
            {
                foreach (var effect in currentFlask._itemEffects)
                    effect.ExecuteEffect(null);
                _flaskLaskCoolDown = Time.deltaTime;
            }
            else
                Debug.Log("Flask On CoolDown");
        }
        else
            Debug.Log("Not equip Flask");
    }

21.5.BUFF制作


public enum StatType
{
    Strength,
    Agility,
    Intelegence,
    Vitality,
    Damage,
    CritChance,
    CritPower,
    Health,
    Armor,
    Evasion,
    MagicRes,
    FireDamage,
    IceDamage,
    LightingDamage
}

[CreateAssetMenu(fileName = "BUFF Effect", menuName = "Data/Item effect/BUFF Effect")]
public class BUFF_Effect : ItemEffect
{
    private PlayerStats _playerStat;
    [SerializeField] private int _modifier;//数值
    [SerializeField] private float _rotetion;
    [SerializeField] private StatType _buffType;
    public override void ExecuteEffect(Transform enemyTransform)
    {
        _playerStat = PlayerManager._instance._player.GetComponent<PlayerStats>();
        //调用BUFF函数增加属性
        _playerStat.IncreaseStatBy(_modifier, _rotetion, StatToModify());
    }
    private Stat StatToModify()
    {
        if (_buffType == StatType.Strength) return _playerStat._strength;
        else if (_buffType == StatType.Agility) return _playerStat._agility;
        else if (_buffType == StatType.Intelegence) return _playerStat._intelligence;
        else if (_buffType == StatType.Vitality) return _playerStat._vitality;
        else if (_buffType == StatType.Damage) return _playerStat._damage;
        else if (_buffType == StatType.CritChance) return _playerStat._critChance;
        else if (_buffType == StatType.CritPower) return _playerStat._critPower;
        else if (_buffType == StatType.Health) return _playerStat._maxHealth;
        else if (_buffType == StatType.Armor) return _playerStat._armor;
        else if (_buffType == StatType.Evasion) return _playerStat._evasion;
        else if (_buffType == StatType.MagicRes) return _playerStat._magicResistance;
        else if (_buffType == StatType.FireDamage) return _playerStat._fireDamage;
        else if (_buffType == StatType.IceDamage) return _playerStat._iceDamage;
        else if (_buffType == StatType.LightingDamage) return _playerStat._lightningDamage;

        return null;
    }
}

使用协程,对目标在一定的时间内增加数值 

    //buff效果, 数值、持续时间、目标
    public virtual void IncreaseStatBy(int modifier, float rotetion, Stat statToModify )
    {
        StartCoroutine(StatModifyCoroutinr(modifier, rotetion, statToModify));  
    }
    protected IEnumerator StatModifyCoroutinr(int modifier, float rotetion, Stat statToModify)
    {
        statToModify.AddModifiers(modifier);
        yield return new WaitForSeconds(rotetion);
        statToModify.RemoveModifiers(modifier);
    }

21.6.护甲装备时间停止

当满足不在冷却和血量低于10%时,才可触发; 

[CreateAssetMenu(fileName = "FreezeEnemies Effect", menuName = "Data/Item effect/FreezeEnemies Effect")]
public class FreezeEnemiesEffect : ItemEffect
{
    [SerializeField] private float _rotetion;//冻结的持续时间

    public override void ExecuteEffect(Transform playerTransform)
    {
        //血量低于10%才会触发
        PlayerStats playerStats = playerTransform.GetComponent<PlayerStats>();
        if (playerStats._currentHealth > playerStats._maxHealth.GetFinishValue() * 0.1f)
            return;
        //在冷却中
        if (!Inventory._Instance.CanUseArmor())
            return;

        Collider2D[] collider2Ds = Physics2D.OverlapCircleAll(playerTransform.position, 2);
        foreach (var hit in collider2Ds)
        {
            //在角色统计的伤害计算里面,判断是否装备了此特效的物品
            hit.GetComponent<Enemy>()?.FreezeTimeFor(_rotetion);
        }
    }
}

22.制作UI菜单 

2

1.在UI画布,添加图像,将源图像设置为目标图像;

2.在页眉使用对应的源图像,并添加Grid Layout Group组件

3.创建按钮并使用好图片和字体

22.1.切换菜单

按钮的点击事件,点击关闭当前游戏对象,激活传入的游戏对象 

    public void SwitchTo(GameObject menu)
    {
        Debug.Log(menu.name);
        //关闭当前菜单
        for(int i = 0; i < transform.childCount; i++)
        {
            transform.GetChild(i).gameObject.SetActive(false);
        }

        if(menu != null)
        {
            //激活点击的菜单
            menu.SetActive(true);
        }
    }

对所有按键添加单击事件,此事件在Canvas的UI脚本的SwitchTo类 

对同一按钮添加同一个菜单游戏对象 

22.2.

更新属性 

using UnityEngine;
using TMPro;

public class UI_StatSlot : MonoBehaviour
{
    [SerializeField] private string _statName;//名字
    [SerializeField] private StatType _statType;//属性类型

    [SerializeField] private TextMeshProUGUI _statValueText;
    [SerializeField] private TextMeshProUGUI _statNameText;

    private void OnValidate()
    {
        //对象名
        gameObject.name = "Stat - " + _statName;

        if(_statNameText != null)
            _statNameText.text = _statName;
    }

    private void Start()
    {
        UpdateStatValueUI();
    }
    //更新属性栏信息
    public void UpdateStatValueUI()
    {
        PlayerStats playerStats = PlayerManager._instance._player.GetComponent<PlayerStats>();

        if(_statValueText != null)
        {
            _statValueText.text = playerStats.GetStat(_statType).GetFinishValue().ToString();
        }
    }
}

无法点击取消此项 :光线投射目标

 

22.3.物品信息显示栏

 显示就是激活次UI对象,隐藏反之

public class UI_ToolTip : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI _itemNameText;
    [SerializeField] private TextMeshProUGUI _itemTypeText;
    [SerializeField] private TextMeshProUGUI _descriptionText;

    [SerializeField] private int _defaultFontSize = 32;
    public void ShowToolTip(ItemDataEquipment item)
    {
        //获取物品信息
        _itemNameText.text = item._itemName;
        _itemTypeText.text = item._itemtype.ToString();
        _descriptionText.text = item.GetDescription();

        //防止字符串过长换行
        if (_itemNameText.text.Length > 14)
            _itemNameText.fontSize = _itemNameText.fontSize * 0.7f;
        else
            _itemNameText.fontSize = _defaultFontSize;

        gameObject.SetActive(true); //显示信息栏
    }
    public void HideToolTip() 
    {
        _itemNameText.fontSize = _defaultFontSize;
        gameObject.SetActive(false);
    }
}

 接口类: IPointerEnterHandler鼠标放置在物体上 IPointerEnterHandler鼠标从物体上移除

//接口类:IPointerDownHandler IPointerEnterHandler鼠标放置在物体上 IPointerEnterHandler鼠标从物体上移除
public class UI_ItemStack : MonoBehaviour , IPointerDownHandler, IPointerEnterHandler, IPointerExitHandler
{
    [SerializeField] protected Image _image;
    [SerializeField] protected TextMeshProUGUI _itemText;//物品数量

    public InventoryItem _item;

    private UI _ui;//为了使用物品信息栏

    void Start()
    {
        _ui = GetComponentInParent<UI>();
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        //为空不调用
        if (_item == null)
            return;
        _ui._toolTip.ShowToolTip(_item._itemData);
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        //为空不调用
        if (_item == null)
            return;
        _ui._toolTip.HideToolTip();
    }
}

添加这两组件,管理显示栏 

如果装备此属性不为零,这添加进StringBuilder中;stringBuilder中的内容最终会添加进 UI创建的描述文本(text)

    //物品描述
    protected StringBuilder _sb = new StringBuilder();

    //输出描述
    public override string GetDescription()
    {
        //清空
        _sb.Clear();
        //不为0的属性添加
        if (_strength != 0) AddItemDescription(_strength, "Strength");
        if (_agility != 0) AddItemDescription(_agility, "Agility");
        if (_intelligence != 0) AddItemDescription(_intelligence, "Intelligence");
        if (_vitality != 0) AddItemDescription(_vitality, "Vitality");

        if (_damage != 0) AddItemDescription(_damage, "Damage");
        if (_critPower != 0) AddItemDescription(_critPower, "CritPower");
        if (_critChance != 0) AddItemDescription(_critChance, "CritChance");
        if (_fireDamage != 0) AddItemDescription(_fireDamage, "FireDamage");
        if (_iceDamage != 0) AddItemDescription(_iceDamage, "IceDamage");
        if (_lightningDamage != 0) AddItemDescription(_lightningDamage, "LightningDamage");

        if (_maxHealth != 0) AddItemDescription(_maxHealth, "MaxHealth");
        if (_armor != 0) AddItemDescription(_armor, "Armor");
        if (_evasion != 0) AddItemDescription(_evasion, "Evasion");
        if (_magicResistance != 0) AddItemDescription(_magicResistance, "MagicResistance");

        return _sb.ToString();
    }

    protected void AddItemDescription(int value, string name)
    {
        //说明有此属性
        if(value != 0)
        {
            //为了第一行不跳行
            if (_sb.Length > 0)
                _sb.AppendLine();

            //添加属性数值
            _sb.Append(name + ": " + value);
        }
    }

 String和stringBuilder区别

String 类:

  • 不可变性:字符串是不可变的,每次对字符串进行修改(如拼接、替换等)时,都会创建一个新的字符串实例。
  • 内存分配:由于不可变性,每次字符串操作都需要在内存堆中为新字符串分配空间,这会导致频繁的内存分配和垃圾回收。
  • 性能:对于单个或少量的字符串操作,性能影响可能不大。但在大量或频繁的字符串连接操作(尤其是循环中),会产生大量的中间字符串,严重影响性能。

StringBuilder 类:

  • 可变性:StringBuilder 是可变的,可以在原对象上直接修改内容,不会生成新的对象。
  • 内存效率:它预先分配了一定大小的缓冲区,并且可以根据需要动态扩展容量,减少了内存分配次数,从而提高了内存使用效率。
  • 性能:适用于处理多个字符串拼接的情况,尤其是在循环或其他需要多次修改字符串的场景下,其性能远优于 String 类。

                        
原文链接:https://blog.csdn.net/qqrrjj2011/article/details/135370510

22.4.角色属性栏

 //像Strength等属性对其他属性有加成,需要计算

    //更新属性栏信息
    public void UpdateStatValueUI()
    {
        PlayerStats playerStats = PlayerManager._instance._player.GetComponent<PlayerStats>();

        if(_statValueText != null)
        {
            _statValueText.text = playerStats.GetStat(_statType).GetFinishValue().ToString();
            
            //像Strength等属性对其他属性有加成,需要计算
            if(_statType == StatType.Health)
            {
                _statValueText.text = playerStats.GetMaxHealth().ToString();
            }
            //力量
            if (_statType == StatType.Damage)
                _statValueText.text = (playerStats._damage.GetFinishValue() + playerStats._strength.GetFinishValue()).ToString();
            if(_statType == StatType.CritPower)
                _statValueText.text = (playerStats._critPower.GetFinishValue() + playerStats._strength.GetFinishValue()).ToString();
            //敏捷
            if(_statType == StatType.Evasion)
                _statValueText.text = (playerStats._evasion.GetFinishValue() + playerStats._agility.GetFinishValue()).ToString();
            if (_statType == StatType.CritChance)
                _statValueText.text = (playerStats._critChance.GetFinishValue() + playerStats._agility.GetFinishValue()).ToString();
            //智力
            if (_statType == StatType.MagicRes)
                _statValueText.text = (playerStats._magicResistance.GetFinishValue() + playerStats._intelligence.GetFinishValue() * 3).ToString();
        }
    }

public class UI_StatsToolTip : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI _description;

    public void ShowStatToolTip(string text)
    {
        _description.text = text;
        gameObject.SetActive(true);
    }
    public void HideStatToolTip()
    {
        _description.text = "";
        gameObject.SetActive(false);
    }
}
public class UI_StatSlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    //属性描述内容
    [TextArea]
    [SerializeField] private string _statText;

    public void OnPointerEnter(PointerEventData eventData)
    {
        _ui._statsToolTip.ShowStatToolTip(_statText);
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        _ui._statsToolTip.HideStatToolTip();
    }
}

22.5.制作信息栏

_craftEquipment将被设置,它里面有所需材料,创建工艺品栏,使用equipment初始化 

public class UI_CraftList : MonoBehaviour, IPointerDownHandler
{
    //为了找所有的工艺品配方
    [SerializeField] private Transform _craftOfParent;
    [SerializeField] private GameObject _craftListPrefab;

    //所有工艺品栏
    [SerializeField] private List<UI_CraftSlot> _craftList;
    //可以制作的工艺品装备已添加
    [SerializeField] private List<ItemDataEquipment> _craftEquipment;

    public void OnPointerDown(PointerEventData eventData)
    {
        SetupCraftList();
    }

    private void AssignCraftList()
    {
        for(int i = 0; i < _craftOfParent.childCount; i++)
        {
            //添加所有工艺品栏
            _craftList.Add(_craftOfParent.GetChild(i).GetComponent<UI_CraftSlot>());
        }
    }

    //
    private void SetupCraftList()
    {
        //删除旧的工艺品栏
        for(int i = 0; i < _craftList.Count; i++)
        {
            Destroy(_craftList[i].gameObject);
        }
        //新空间
        _craftList = new List<UI_CraftSlot>();
        
        //使用已添加的装备成品,生成新工艺品栏
        for(int i = 0; i < _craftEquipment.Count; i++)
        {
            Debug.Log(i);
            Debug.Log(_craftEquipment[i]._itemName);
            GameObject newSlot = Instantiate(_craftListPrefab, _craftOfParent);
            newSlot.GetComponent<UI_CraftSlot>().SetupCraftSlot(_craftEquipment[i]);
        }
        AssignCraftList();
    }
    void Start()
    {
        AssignCraftList();
    }
}

 工艺品栏

public class UI_CraftSlot : UI_ItemStack
{

    public void SetupCraftSlot(ItemDataEquipment equipment)
    {
        if (equipment == null)
            return;

        _item._itemData = equipment;
        //设置图片和名字
        _itemIcon.sprite = equipment._icon;
        _itemText.text = equipment._itemName;
    }
    public override void OnPointerDown(PointerEventData eventData)
    {
        ItemDataEquipment itemToCraft = _item._itemData as ItemDataEquipment;
        //点击则会制造物品
        if (Inventory._Instance.CanCraft(itemToCraft, itemToCraft._craftMaterials))
        {
            //制作成功可以播放成功欢快的音乐;
        }
    }
}

public class UI_CraftList : MonoBehaviour, IPointerDownHandler
{
    //为了找所有的工艺品配方
    [SerializeField] private Transform _craftOfParent;
    [SerializeField] private GameObject _craftListPrefab;

    //所有工艺品栏
    [SerializeField] private List<UI_CraftSlot> _craftList;
    //可以制作的工艺品装备已添加
    [SerializeField] private List<ItemDataEquipment> _craftEquipment;

    public void OnPointerDown(PointerEventData eventData)
    {
        SetupCraftList();
    }

    private void AssignCraftList()
    {
        for(int i = 0; i < _craftOfParent.childCount; i++)
        {
            //添加所有工艺品栏
            _craftList.Add(_craftOfParent.GetChild(i).GetComponent<UI_CraftSlot>());
        }
    }

    //
    private void SetupCraftList()
    {
        //删除旧的工艺品栏
        for(int i = 0; i < _craftList.Count; i++)
        {
            Destroy(_craftList[i].gameObject);
        }
        //新空间
        _craftList = new List<UI_CraftSlot>();
        
        //使用已添加的装备成品,生成新工艺品栏
        for(int i = 0; i < _craftEquipment.Count; i++)
        {
            GameObject newSlot = Instantiate(_craftListPrefab, _craftOfParent);
            newSlot.GetComponent<UI_CraftSlot>().SetupCraftSlot(_craftEquipment[i]);
        }
        AssignCraftList();
    }
    void Start()
    {
        AssignCraftList();
    }
}

 

public class UI_CraftWindow : MonoBehaviour
{
    [SerializeField] private Image _image;//武器图标
    [SerializeField] private TextMeshProUGUI _nameText;//名字
    [SerializeField] private TextMeshProUGUI _statText;//属性描述

    [SerializeField] private Image[] _MaterialImages;//材料图片
    [SerializeField] private Button _craftButton;//按钮

    public void SetupCraftWindow(ItemDataEquipment equipment)
    {
        _craftButton.onClick.RemoveAllListeners();
        //材料图片和名字先透明
        for(int i = 0; i < _MaterialImages.Length; i++)
        {
            _MaterialImages[i].color = Color.clear;
            _MaterialImages[i].GetComponentInChildren<TextMeshProUGUI>().color = Color.clear;
        }

        //材料数量超过所需材料栏的数量,那么是不合法的
        if (equipment._craftMaterials.Count > _MaterialImages.Length)
        {
            Debug.LogWarning("You have materials amount than you have material slots in craft window");
            return;
        }

        //设置图标、名字、属性描述
        _image.sprite = equipment._itemIcon;
        _nameText.text = equipment._itemName;
        _statText.text = equipment.GetDescription();

        //将所需材料添加进所需材料栏
        for (int i = 0; i < equipment._craftMaterials.Count; i++) 
        {
            _MaterialImages[i].sprite = equipment._craftMaterials[i]._itemData._itemIcon;
            _MaterialImages[i].GetComponentInChildren<TextMeshProUGUI>().text = equipment._craftMaterials[i]._stackSize.ToString();

            _MaterialImages[i].color = Color.white;
            _MaterialImages[i].GetComponentInChildren<TextMeshProUGUI>().color = Color.white;
        }
        _craftButton.onClick.AddListener(() => Inventory._Instance.CanCraft(equipment, equipment._craftMaterials));

    }
}

22.6.制作技能树

只能树的分支有两种:

  1. 等级提升(一条直线)
  2. 技能分支(多选一)

    //技能是否解锁
    [SerializeField] private bool _unlocked;
    //所需要的解锁技能
    [SerializeField] private UI_SkillTreeSlot[] _shouldBeUnlocked;
    //所需要的非解锁技能
    [SerializeField] private UI_SkillTreeSlot[] _shouldBeLocked;

    private Image _skillImage;
    private void OnValidate()
    {
        gameObject.name = "SkillTreeSlot - " +  _skillName;
    }
    void Start()
    {
        _skillImage = GetComponent<Image>();
        GetComponent<Button>().onClick.AddListener(UnlockSkillSlot);

    }
    public void UnlockSkillSlot()
    {
        //等级解锁(2级需要1级才能解锁)
        for (int i = 0; i < _shouldBeUnlocked.Length; i++)
        {
            if (_shouldBeUnlocked[i]._unlocked == false)
            {
                Debug.Log("Can't unlocked skill");
                return;
            }
        }

        //分支解锁,多选一(选择一个解锁方向)
        for (int i = 0; i < _shouldBeLocked.Length; i++)
        {
            if (_shouldBeLocked[i]._unlocked == true)
            {
                Debug.Log("Can't unlocked skill");
                return;
            }
        }

        //满足条件解锁技能
        _unlocked = true;
        _skillImage.color = Color.red;
    }

 技能描述面板

public class UI_SkillToolTip : MonoBehaviour
{
    //名字和描述
    [SerializeField] private TextMeshProUGUI _skillName;
    [SerializeField] private TextMeshProUGUI _skillText;

    public void ShowToolTip(string skillName, string skillText)
    {
        //写入名字和描述
        _skillName.text = skillName;
        _skillText.text = skillText;

        gameObject.SetActive(true);//激活提示
    }
    public void HideToolTip() => gameObject.SetActive(false);

}

22.7.技能树和技能关联

 当激活按钮时;因为下面的start添加了事件

    [Header("Dash")]
    public bool _dashUnlock;
    [SerializeField] private UI_SkillTreeSlot _dashUnlockButton;


    protected override void Start()
    {
        base.Start();
        //添加按钮事件
        _dashUnlockButton.GetComponent<Button>().onClick.AddListener(UnlockDash);
    }
    //解锁技能
    public void UnlockDash()
    {
        if(_dashUnlockButton._unlocked)
            _dashUnlock = true;
    }

22.7.技能冷却效果制作

 

public class UI_InGame : MonoBehaviour
{
    [Header("Skill Cooldown")]
    //冲刺技能冷却
    [SerializeField] private Image _dashImage;
    [SerializeField] private float _dashCooldown;


    void Start()
    {

        //初始化技能冷却
        _dashCooldown = SkillManager._instance._dash.GetCooldown();

    }
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.LeftShift))
            CheckCooldownOf(_dashImage);
        SetCooldown(_dashImage);

    }

    //冷却是否完毕
    private void CheckCooldownOf(Image image)
    {
        if (image && image.fillAmount <= 0)
            image.fillAmount = 1;
    }

    //设置冷却
    private void SetCooldown(Image image)
    {
        if(image && image.fillAmount > 0)
            image.fillAmount -=  1 / _dashCooldown * Time.deltaTime; 
    }
}

23.保存系统

23.1.游戏数据基础的保存和加载

游戏开始是创建一个新的游戏数据类来保存数据,开始读取文件内的数据,退出时调用保存函数,保存游戏数据

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class SaveManager : MonoBehaviour
{
    public static SaveManager _instance;//单例

    public GameData _gameData;//游戏数据

    public List<ISaveManagr> _saveManagerList;//获取所有使用包含此接口的类、

    private FileDataHandler _fileDataHandler;//序列化和读写文件
    [SerializeField] string _fileName;//保存文件名

    private void Awake()
    {
        if( _instance == null )
            _instance = this;
        else
            Destroy( _instance.gameObject );
    }

    private void Start()
    {
        _saveManagerList = FindAllSaveManager();//获取所有使用包含此接口的类
        _fileDataHandler = new FileDataHandler(Application.persistentDataPath, _fileName);
        NewGameData();
        LoadGameData();
    }

    private void NewGameData()
    {
        _gameData = new GameData();
    }

    //加载游戏数据
    private void LoadGameData()
    {
        _gameData = _fileDataHandler.Load();
        if(_gameData == null)
        {
            NewGameData();
            Debug.Log("No save data found");
        }

        //加载所有类包含此接口的游戏数据
        foreach(ISaveManagr saveManagr in _saveManagerList)
        {
            saveManagr.LoadGameData(_gameData);
        }
        Debug.Log("Load currency" + _gameData._curency);
    }

    //保存游戏数据
    private void SaveGameData()
    {
        _fileDataHandler.Save(_gameData);
        //保存所有包含此接口的类的游戏数据
        foreach(ISaveManagr saveManagr in _saveManagerList )
        {
            saveManagr.SaveGameData(ref _gameData);
        }
        Debug.Log("Game data was saved");
        Debug.Log("Save currency" + _gameData._curency);
    }

    //程序退出保存数据
    private void OnApplicationQuit()
    {
        SaveGameData();
    }

    //获取所有包含此接口的类
    private List<ISaveManagr> FindAllSaveManager()
    {
        IEnumerable<ISaveManagr> saveManagrs = FindObjectsOfType<MonoBehaviour>().OfType<ISaveManagr>();
        return new List<ISaveManagr>(saveManagrs);
    }
}

 游戏数据类

[System.Serializable]
public class GameData 
{
    public int _curency;
    
    public GameData()
    {
        _curency = 0;
    }
}

23.1.2.批量调用 使用同一个接口实现的函数 

    public List<ISaveManagr> _saveManagerList;//获取所有使用包含此接口的类、


    private void Start()
    {
        _saveManagerList = FindAllSaveManager();//获取所有使用包含此接口的类

    }
    //获取所有包含此接口的类
    private List<ISaveManagr> FindAllSaveManager()
    {
        IEnumerable<ISaveManagr> saveManagrs = FindObjectsOfType<MonoBehaviour>().OfType<ISaveManagr>();
        return new List<ISaveManagr>(saveManagrs);
    }

23.2.文件的序列化和IO

public class FileDataHandler
{
    //文件保存目录路径
    private string _dataDirPath;

    //文件保存名字
    private string _dataFileName;

    public FileDataHandler(string dataDirPath, string dataFileName)
    {
        _dataDirPath = dataDirPath;
        _dataFileName = dataFileName;
    }

    public void Save(GameData gameData )
    {
        //组合成一个路径:目录 + 文件名
        string fullPath = Path.Combine( _dataDirPath, _dataFileName );

        try
        {
            //创建目录文件
            Directory.CreateDirectory( Path.GetDirectoryName(fullPath) );

            //序列化数据
            string dataToStore = JsonUtility.ToJson( gameData , true);

            //创建文本文件
            using(FileStream fs = new FileStream(fullPath, FileMode.Create))
            {
                //以写入的方式打开
                using(StreamWriter sw = new StreamWriter(fs))
                {
                    //写入数据
                    sw.Write(dataToStore);
                }
            }
        }
        catch(Exception e)
        {
            Debug.LogError("Error trying to save data to feil: " + fullPath + '\n' +  e);
        }
    }

    public GameData Load()
    {
        string fullPath = Path.Combine(_dataDirPath, _dataFileName);

        GameData loadData = null;

        //有保存的文件
        if(File.Exists(fullPath))
        {
            try
            {
                string dataToStore = "";
                //打开文件
                using(FileStream fs = new FileStream(fullPath, FileMode.Open))
                {
                    //以读取文件的方式打开
                    using(StreamReader sr = new StreamReader(fs))
                    {
                        //读取存储的数据
                        dataToStore = sr.ReadToEnd();
                    }
                }
                loadData = JsonUtility.FromJson<GameData>(dataToStore);
            }
            catch (Exception e)
            {
                Debug.LogError("Error trying to load data to feil: " + fullPath + '\n' + e);
            }

        }
        return loadData;
    }
}

23.2.1.字典如何序列化

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//使字典能序列化
[System.Serializable]
public class SerializableDictionary<TKey, TValue> :Dictionary<TKey, TValue>, ISerializationCallbackReceiver
{
    [SerializeField]private List<TKey> _keys = new List<TKey>();//保存key值
    [SerializeField] private List<TValue> _values = new List<TValue>();//保存value值

    //序列化之前
    public void OnAfterDeserialize()
    {
        _keys.Clear();
        _values.Clear();

        foreach(KeyValuePair<TKey, TValue> kv in this)
        {
            _keys.Add(kv.Key);
            _values.Add(kv.Value);
        }
    }
    //反序列化之后
    public void OnBeforeSerialize()
    {
        this.Clear();

        //kv的数量不对等
        if(_keys.Count != _values.Count)
        {
            Debug.Log("key count is not equal to value count");
        }
        else
        {
            //反序列化的数据添加进字典
            for(int i =  0; i < _keys.Count; i++)
            {
                this.Add(_keys[i], _values[i]);
            }
        }
    }
}

23.2.2.如何获取所有资源

    [Header("Data Base")]
    private List<ItemData> _itemDataBase;
    public List<InventoryItem> _loadDataBase;
    //设置数据库(所有资源),并获取它
    private List<ItemData> GetItemDataBase()
    {
        _itemDataBase = new List<ItemData>();
        string[] _assetName = AssetDatabase.FindAssets( "", new[] {"Assets/ItemData/Equipment"} );//返回GUID

        foreach(var SOName in _assetName)
        {
            var SOPath = AssetDatabase.GUIDToAssetPath( SOName );//获取路径
            var itemData = AssetDatabase.LoadAssetAtPath<ItemData>(SOPath);//加载资源
            _itemDataBase.Add(itemData);//添加进数据库
        }
        return _itemDataBase;
    }

加载保存的资源,比对本地资源库,获取对应资源 

    public void LoadGameData(GameData gameData)
    {
        //需要加载的资源
        foreach(KeyValuePair<String, int> pair in gameData._inventory)
        {
            //数据库的数据
            foreach(var item in _itemDataBase)
            {
                //不是文件夹
                if(item != null && item._itemID == pair.Key)
                {
                    InventoryItem itemToLoad = new InventoryItem(item);
                    itemToLoad._stackSize = pair.Value;
                    _loadDataBase.Add(itemToLoad);//添加进加载数据库
                }
            }
        }
    }

22.3.3. 保存加载物品和已装备的装备

加载物品和已装备的装备

    public List<InventoryItem> _loadDataBase;//物品
    public List<ItemDataEquipment> _loadEquipments;//已装备的装备

    private void AddStartingItem()
    {
        //加载已装备物品
        foreach(ItemDataEquipment equipment in _loadEquipments)
        {
            EquipItem(equipment);
        }

        if(_loadDataBase.Count > 0)
        {
            foreach(var item in _loadDataBase)
            {
                for(int i = 0; i < item._stackSize; i++)
                {
                    AddItem(item._itemData);
                }
            }
            return;//第一次会添加初始物品,后续则不需要
        }

        //添加初始物品
        for (int i = 0; i < _startingItem.Count; i++)
        {
            if (_startingItem[i] != null )
                AddItem(_startingItem[i]);
        }
    }

    public void LoadGameData(GameData gameData)
    {
        //需要加载的物品
        foreach(KeyValuePair<String, int> pair in gameData._inventory)
        {
            //数据库的数据
            foreach(var item in GetItemDataBase())
            {
                //不是文件夹
                if(item != null && item._itemID == pair.Key)
                {
                    InventoryItem itemToLoad = new InventoryItem(item);
                    itemToLoad._stackSize = pair.Value;
                    _loadDataBase.Add(itemToLoad);//添加进加载数据库
                }
            }
        }

        //已装备的装备
        foreach (string equiment in gameData._equipmentID)
        {
            foreach(var item in GetItemDataBase())
            {
                if(item != null && equiment == item._itemID )
                {
                    //添加进加载链表
                    _loadEquipments.Add(item as ItemDataEquipment);
                }
            }
        }

    }

    //设置数据库(所有资源),并获取它
    private List<ItemData> GetItemDataBase()
    {
        List<ItemData> _itemDataBase = new List<ItemData>();
        string[] _assetName = AssetDatabase.FindAssets( "", new[] {"Assets/ItemData/Items"} );//返回GUID

        foreach(var SOName in _assetName)
        {
            var SOPath = AssetDatabase.GUIDToAssetPath( SOName );//获取路径
            var itemData = AssetDatabase.LoadAssetAtPath<ItemData>(SOPath);//加载资源
            _itemDataBase.Add(itemData);//添加进数据库
        }
        return _itemDataBase;
    }

22.3.4.保存加载技能树

界面技能栏解锁 加载保存技能

    public void LoadGameData(GameData gameData)
    {
        if (gameData._skillTree.TryGetValue(_skillName, out bool value))
            _unlocked = value;
    }
    public void SaveGameData(ref GameData gameData)
    {
        //已经存在,删除在插入
        if (gameData._skillTree.TryGetValue(_skillName, out bool value))
        {
            gameData._skillTree.Remove(_skillName);
            gameData._skillTree.Add(_skillName, _unlocked);
        }
        else
            gameData._skillTree.Add(_skillName, _unlocked);
    }

技能根据界面技能树的解锁与否,解锁技能(在基类的start函数调用)

    //加载技能树时,测试技能是否解锁
    protected override void CheckUnlock()
    {
        UnlockMirage();
        UnlockAggresive();
        UnlockDuplicate();
        UnlockCrystalInstead();
    }

22.3.5.加密数据

    //加密密钥
    private bool _encryptData = false;
    private string _codeWord = "mingtianhuigenghao";    
    private string EncryptDecrypt(string data)

    {
        string modifiedData = "";

        //异或相同值两次会变回原来的值
        for (int i = 0; i < data.Length; i++)
            modifiedData += (char) data[i] ^ _codeWord[i % _codeWord.Length];

        return modifiedData;
    }

24.主菜单

对场景生成设置

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class UI_MainMenu : MonoBehaviour
{
    private string _mainScene = "MainScene";//主场景
    [SerializeField] private GameObject _continueButton;//继续游戏按钮

    private void Start()
    {
        if (SaveManager._instance.HasGameData())
            _continueButton.SetActive(true);
    }

    public void ContinueGame()
    {
        SceneManager.LoadScene(_mainScene);
    }

    public void NewGame()//新游戏,即删除保存文件
    {
        SaveManager._instance.DeleteSaveData();
        SceneManager.LoadScene(_mainScene);
    }

    public void ExitGame()
    {
        Debug.Log("Exit game");
        //Application.Quit();
    }

}

24.1.黑屏浅入浅出效果

public class UI_DarkScreenFade : MonoBehaviour
{
    private Animator _anim;

    private void Start()
    {
        _anim = GetComponent<Animator>();
    }

    public void FadeOut() => _anim.SetBool("FadeOut", true);
    public void FadeIn() => _anim.SetBool("FadeIn", true);
}

制作黑屏效果 

24.2.死亡重新游戏

using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public static GameManager _instance;

    private void Awake()
    {
        if (_instance == null)
            _instance = this;
        else
           Destroy(gameObject);
    }
    //重新开始
    public void RestartScene()
    {
        Scene scene = SceneManager.GetActiveScene();//当前场景
        SceneManager.LoadScene(scene.name);
    }
}

24.3.货币快速增长减少效果

    //当前货币值
    [Header("Currency Info")]
    [SerializeField] private TextMeshProUGUI _currentCurrency;
    [SerializeField] private float _amountOfCurrency;
    [SerializeField] private float _increaseRate;

    private void UpdateCurrencyUI()
    {
        //和货币值比较
        if (_amountOfCurrency < PlayerManager._instance.GetCurrentCurency())
            _amountOfCurrency += _increaseRate * Time.deltaTime;
        else
            _amountOfCurrency = PlayerManager._instance.GetCurrentCurency();

        //货币快速增长效果
        _currentCurrency.text = ((int)_amountOfCurrency).ToString("#,#");
    }

25.记录点


public class CheckPoint : MonoBehaviour
{
    private Animator _anim;
    public string _checkPointId;//检测点id
    public bool _active;//是否开启检测点

    private void Start()
    {
        _anim = GetComponent<Animator>();
    }

    [ContextMenu("Generate Checkpoint id")]
    private void GenerateId()
    {
        _checkPointId = System.Guid.NewGuid().ToString();   
    }

    //碰撞解锁,只有碰撞检测没有碰撞效果,只是OnCollisionEnter的一部分效果
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.GetComponent<Player>() != null)
            ActiveCheckPointer();
    }

    public void ActiveCheckPointer()
    {
        _active = true;
        _anim.SetBool("Active", true);  
    }
}

本地保存记录点 

public class GameManager : MonoBehaviour, ISaveManagr
{
    public static GameManager _instance;
    [SerializeField] private CheckPoint[] _checkPoints;//监测点集合

    private void Awake()
    {
        if (_instance == null)
            _instance = this;
        else
           Destroy(gameObject);
    }

    //获取所有的检测点
    private void Start()
    {
        _checkPoints = FindObjectsOfType<CheckPoint>();
    }

    //重新开始
    public void RestartScene()
    {
        Scene scene = SceneManager.GetActiveScene();//当前场景
        SceneManager.LoadScene(scene.name);
    }

    //保存检测点
    public void SaveGameData(ref GameData gameData)
    {
        //先清空数据
        gameData._checkpoints.Clear();

        foreach(CheckPoint checkPoint in _checkPoints)
        {
            gameData._checkpoints.Add(checkPoint._checkPointId, checkPoint._active);
        }
    }

    public void LoadGameData(GameData gameData)
    {
        //读取本地数据
        foreach (var checkPoint in gameData._checkpoints)
        {
            //比对Guid
            for (int i = 0; i < _checkPoints.Length; i++)
            {
                if (checkPoint.Key == _checkPoints[i]._checkPointId && checkPoint.Value == true)
                    _checkPoints[i].ActiveCheckPointer();
            }
        }
    }
}

找到最近记录点

    //保存检测点
    public void SaveGameData(ref GameData gameData)
    {

        gameData._closestCheckPoint = ClosestCheckPoint()?._checkPointId;
    }

    public void LoadGameData(GameData gameData)
    {
        //角色移动最近记录点
        foreach(CheckPoint checkPoint in _checkPoints)
        {
            //通过Guid找到对应位置
            if(gameData._closestCheckPoint == checkPoint._checkPointId)
                PlayerManager._instance._player.transform.position = checkPoint.transform.position;
        }
    }

    //找到最近记录点
    private CheckPoint ClosestCheckPoint()
    {
        CheckPoint ClosestCheckPoint = null;
        float closestDistance = Mathf.Infinity;

        foreach(CheckPoint checkPoint in _checkPoints)
        {
            float currentClosest = Vector2.Distance(checkPoint.transform.position, PlayerManager._instance._player.transform.position);

            //距离更近且处于激活,更新值
            if(currentClosest < closestDistance && checkPoint._active)
            {
                ClosestCheckPoint = checkPoint;
                closestDistance = currentClosest;
            }
        
        return ClosestCheckPoint;
    }

26.音频管理

26.1.基本逻辑

为所有音效和BGM创建一个父物体,并关闭唤醒时播放

一直同一个音效使人感到枯燥,可以是试着降低或者升高音高

基本逻辑

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AudioManager : MonoBehaviour
{
    public static AudioManager _instance;

    [SerializeField] private AudioSource[] _sfx;//音效集合
    [SerializeField] private AudioSource[] _bgm;//背景音乐集合

    private bool _playBGM;//是否需要播放
    private int _bgmIndex;//当前正在播放

    private void Awake()
    {
        if (_instance == null)
            _instance = this;
        else
            Destroy(this.gameObject);
    }

    private void Update()
    {
        if(_playBGM == false)
            StopAllBGM();
        else
        {
            //需要播放的BGM正在播放吗
            if (!_bgm[_bgmIndex].isPlaying)
                PlayBGM(_bgmIndex);
        }
    }

    //打开某个音效
    public void PlaySFX(int index)
    {
        if( index < _sfx.Length)
        {
            _sfx[index].pitch = Random.Range(0.85f, 1.2f);//升高或者降低音高,形成差异化
            _sfx[index].Play();
        }
    }
    //关闭某个音效
    public void StopSFX(int index)
    {
        if( index < _sfx.Length)
        {
            _sfx[index].Stop();
        }
    }
    //开启某个BGM(一次只能有一个BGM)
    public void PlayBGM(int index)
    {
        _bgmIndex = index;
        //关闭所有
        StopAllBGM();

        _bgm[_bgmIndex].Play();
    }
    
    //随机播放BGM
    public void PlayRandomBGM()
    {
        int index = Random.Range(0, _bgm.Length);
        PlayBGM(index);
    }

    //关闭所有BGM
    public void StopAllBGM()
    {
        for(int i = 0; i < _bgm.Length; i++)
        {
            _bgm[i].Stop();
        }
    }
}

26.2.控制播放距离

    [SerializeField] private float _sfxMinimumdistance;//音效最远播放距离

    //打开某个音效
    public void PlaySFX(int index, Transform targetTransform)
    {
        //超出范围,已经在播放的不用再播放
        if (index > _sfx.Length || _sfx[index].isPlaying)
            return;

        //比较最远播放距离
        if (targetTransform != null && _sfxMinimumdistance < (Vector2.Distance(PlayerManager._instance._player.transform.position, targetTransform.position)))
            return;

        _sfx[index].pitch = Random.Range(0.85f, 1.2f);//升高或者降低音高,形成差异化
        _sfx[index].Play();
    }

26.3.音量控制

1.创建一个音频混合器

2.将所有的背景音乐和音效添加进对应的音频组

3.音频控制的原理:将滑块和音频组关联

4.代码逻辑

使用对数使范围在【0,-3】

public class UI_VolumnSlider : MonoBehaviour
{
    public Slider _slider;//音量滑块
    public string _parameter;//参数名字

    [SerializeField] private AudioMixer _mixer;//
    [SerializeField] private float _multiplier;//乘数

    public void SliderValue(float value)
    {
        _mixer.SetFloat(_parameter, Mathf.Log10(value) * _multiplier);//设置音量
    }
}

26.4.音量值保存

G阿么Data类 

    //音量值
    public SerializableDictionary<string, float> _volumeSetting;

    public GameData()
    {
        _volumeSetting = new SerializableDictionary<string, float>();
    }

UI类 

    //音量值集合
    [SerializeField] private UI_VolumnSlider[] _volumnSlider;

   //保存音量值
   public void SaveGameData(ref GameData gameData)
   {
       gameData._volumeSetting.Clear();

       foreach(UI_VolumnSlider volumeValue in _volumnSlider)
       {
           gameData._volumeSetting.Add(volumeValue._parameter, volumeValue._slider.value);
       }
   }

   //加载音量值
   public void LoadGameData(GameData gameData)
   {
       foreach(KeyValuePair<string, float> kv in gameData._volumeSetting)
       {
           foreach(UI_VolumnSlider volume in _volumnSlider)
           {
               if (kv.Key == volume._parameter)
                   volume.LoadVolume(kv.Value);
           }
       }
   }

26.5.区域音乐

1.创建空对象

 2.触发逻辑

public class AreaSound : MonoBehaviour
{
    [SerializeField] private int _areaSoundIndex;

    //进入某一个区域播放区域音乐
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.GetComponent<Player>() != null)
            AudioManager._instance.PlaySFX(_areaSoundIndex, null);
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if(collision.GetComponent<Player>() != null)
            AudioManager._instance.StopSFXWithTime(_areaSoundIndex);
    }
}

3.随时间减少音量,小于0.1停止音乐,恢复默认值

    //随时间音乐逐渐停止,区域音效使用
    public void StopSFXWithTime(int index) => StartCoroutine(DecreaseVolume(index));
    //随时间减小音量
    private IEnumerator DecreaseVolume(int index)
    {
        float defualtVolume  = _sfx[index].volume;
        Debug.Log(defualtVolume);

        while (_sfx[index].volume > 0.1f)
        {
            _sfx[index].volume -= 0.2f;//减小音量

            yield return new WaitForSeconds(0.5f);

            if (_sfx[index].volume < 0.1f)//快没有声音了,停止
            {
                _sfx[index].volume = defualtVolume;
                StopSFX(index);
                break;
            }

        }
    }

26.6.如何找到一些音效资Unity Store, itch.io;

27.抛光

27.1.正确对目标击退

1.敌人的击退方向

    private int _knockbackDir;//击退方向

    public void SetKnockbackDirection(Transform target)
    {
        if (transform.position.x < target.position.x)
            _knockbackDir = -1;
        else
            _knockbackDir = 1;
    }
    private IEnumerator HitKnockback()
    {
        isKnock = true;
        _rb.velocity = new Vector2(_knockbackDirection.x * _knockbackDir, _knockbackDirection.y);
        yield return new WaitForSeconds(_knockbackDuring);
        isKnock = false;
    }

2.玩家的击退效果:只有单次伤害超过0.3,才会被击退

    public void SetupKnockbackPower(Vector2 power) => _knockbackPower = power;
    //玩家击退力设置为0
    protected virtual void SetupZeroKnockbackPower()
    {}

重写方法

    //击退力设置为0
    protected override void SetupZeroKnockbackPower()
    {
        _knockbackPower = new Vector2(0, 0);
    }

伤害超过百分之30才会击退

    protected override void DecareaseHealthBy(int damage)
    {
        base.DecareaseHealthBy(damage);

        //攻击超过最大生命的百分之30,将被击退
        if(damage > Mathf.RoundToInt(_maxHealth.GetFinishValue()* 0.3f ) )
        {
            int randomSound = Random.Range(33, 34);
            AudioManager._instance.PlaySFX(randomSound, null);

            _player.SetupKnockbackPower(new Vector2(8, 4));
        }

        //装备特效
        ItemDataEquipment armor = Inventory._instance.GetEquipment(EquipmentType.Armor);
        if (armor != null)
            armor.ItemEffect(PlayerManager._instance._player.transform);
    }

27.2.在打开菜单是暂停游戏

timeScale时间刻度,为1和实时流逝速度相同,0则基本上为暂停状态,0.5为实时流逝速度的0.5倍

GameManager类 

    //timeScale时间刻度,为1和实时流逝速度相同,0则基本上为暂停状态,0.5为实时流逝速度的0.5倍
    public void PauseGame(bool pause)
    {
        if(pause == true)
            Time.timeScale = 0;
        else
            Time.timeScale = 1;
    }

 UI类

        //使用timeScale暂停游戏
        if(GameManager._instance != null)
        {
            if (menu == _inGame_UI)
                GameManager._instance.PauseGame(false);
            else
                GameManager._instance.PauseGame(true);
        }

Player类 

    protected override void Update()
    {
        //打开菜单暂停游戏中
        if(Time.timeScale == 0)
            return;
    }

27.3.坠入虚空死亡 

在会掉落的位置,放置碰撞器

public class DeadZone : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.GetComponent<CharacterStats>() != null )
            collision.GetComponent<CharacterStats>().KillEntity();
        else
            Destroy(collision.gameObject);
    }
}

27.粒子效果使用

27.1.在角色上使用粒子效果

27.2.下雪效果 

形状为一个巨大的box,在里面生成大量的粒子效果,缩小粒子且给他向下的速度;和玩家、敌人、地面有碰撞效果

27.3.尘埃效果

  • 将持续时间设为0.1f,发射粒子数量设为500,将只发射一次大量粒子 ;
  • 将起始生命周期设为更短(1.5f),模拟速度设为2会以更快速度移动;
  • 模拟空间设为世界,碰撞设为everything

28.特效

28.打击特效

普通攻击和暴击的特效不同,使用不同的动画,对旋转和朝向进行一定的调整

    //打击星型特效
    public void CreateHitFX(Transform target, bool critical)
    {
        //增加一点随机性
        float zRotate = Random.Range(-90, 90);//是星型特效旋转一点角度
        float xPosition = Random.Range(-0.2f, 0.5f);//位置
        float yPosition = Random.Range(-0.5f, 0.5f);

        Vector3 hitFXRotate = new Vector3(0, 0, zRotate);
        GameObject hitFx = _StarHitFX;

        //如果暴击,那么设置为暴击特效,修改选择
        if (critical)
        {
            hitFx = _criticalHitFX;

            zRotate = Random.Range(-45, 45);
            float yRotate = 0;
            if(GetComponentInParent<Entity>()._facingDir == -1)//特效朝向
                yRotate = 180;
            hitFXRotate = new Vector3(0, yRotate, zRotate);
        }
        GameObject newHitFX = Instantiate(hitFx, target.position + new Vector3(xPosition, yPosition), Quaternion.identity);
        newHitFX.transform.Rotate(hitFXRotate);//旋转一定角度

        Destroy(newHitFX, 0.5f);

    }
}

29.冲刺残影效果,如果放置在PlayerState上那么就类似造梦西游无双效果

残影效果设置

public class AfterImage_FX : MonoBehaviour
{
    private SpriteRenderer _sr;
    private float _colorLooseRate;

    public void SetupAfterImage(Sprite sprite, float colorLooseRate)
    {
        _sr = GetComponent<SpriteRenderer>();
        _sr.sprite = sprite;
        _colorLooseRate = colorLooseRate;
    }

    private void Update()
    {
        //减少alpha值
        float alpha = _sr.color.a - _colorLooseRate * Time.deltaTime;
        _sr.color = new Color(_sr.color.r, _sr.color.g, _sr.color.b, alpha);

        if(_sr.color.a <= 0 )//alpha值小于0,销毁对象
            Destroy(gameObject);
    }
}

创建残影效果并对他进行调整 

    //冲刺残影效果
    public void AfterImageFX()
    {
        if(_afterImageCoolDownTimer <= 0)
        {
            _afterImageCoolDownTimer = _afterImageCoolDown;
            GameObject newGameObject = Instantiate(_afterIamgePrefab, transform.position, Quaternion.identity);

            if(PlayerManager._instance._player._facingDir == -1)
                newGameObject.transform.Rotate(new Vector3(0, 180, 0));
            newGameObject.GetComponent<AfterImage_FX>().SetupAfterImage(_sr.sprite, _colorLooseRate);//使用_sr会获取当前玩家的图片
        }
    }

30.窗口抖动 

1.添加监听 

using Cinemachine;

    //窗口抖动效果
    private CinemachineImpulseSource _screenShack;
    [Header("Screen Shack FX")]
    [SerializeField] private float _shackMultiplier;
    public Vector3 _sowrdShackPower;//抓住剑 使屏幕抖动的力
    public Vector3 _highHitShackPower;//受到高伤害 使屏幕抖动的力

    //屏幕抖动效果
    public void ScreenShack(Vector3 shackPower)
    {
        _screenShack.m_DefaultVelocity = new Vector3(shackPower.x, shackPower.y) * _shackMultiplier;
        _screenShack.GenerateImpulse();
    }

2.添加脚本 

31.弹出式窗口

1.使用3D对象的文本,非UI里面的

2.弹出文本逻辑:生命周期内,缓慢移动,生命周期结束减少Alpha值且快速移动,Alpha为0删除文本

using TMPro;
using UnityEngine;

public class PopUpText_FX : MonoBehaviour
{
    private TextMeshPro _myText;

    [SerializeField] private float _speed;//文本移动速度
    [SerializeField] private float _desappearanceSpeed;//生命周期结束后的速度
    [SerializeField] private float _colorDesappearanceSpeed;//颜色失去速度
    [SerializeField] private float _lifeTime;//生命周期

    private float _textTimer;

    private void Start()
    {
        _myText = GetComponent<TextMeshPro>();
        _textTimer = _lifeTime;
    }

    private void Update()
    {
        transform.position = Vector2.MoveTowards(transform.position, new Vector2(transform.position.x, transform.position.y + 1), _speed * Time.deltaTime);
        _textTimer -= Time.deltaTime;
        if(_textTimer < 0)
        {
            float alpha = _myText.color.a - _colorDesappearanceSpeed * Time.deltaTime;
            _myText.color = new Color(_myText.color.r, _myText.color.g, _myText.color.b,alpha);//写入alpha值

            if (alpha < 50) //透明度到一定程度,移动
                _speed = _desappearanceSpeed;
            if(alpha < 0)
                Destroy(gameObject);
        }
    }
}

3 .调用创建文本函数,传入需要使用的字符串即可

    //弹出文本效果
    public void CreatePPopUpText(string text)
    {
        float randomX = Random.Range(-1, 1);
        float randomy = Random.Range(2, 4);
        Vector3 positionOffset = new Vector3(randomX, randomy, 0);

        //实例预制体
        GameObject newText = Instantiate(_popUpTextprefab, transform.position + positionOffset, Quaternion.identity);
        newText.GetComponent<TextMeshPro>().text = text;//填写字符串
    }

29.生成和构建

去除多余的头文件

遇到的问题如下:

    public List<ItemData> _itemDataBase;//因AssetDatabase类在运行时不可使用,所以在unity编辑器中提前调用,拿到所有物品;

#if UNITY_EDITOR//预编译指令:仅在unity编辑器阶段使用
    //因AssetDatabase类在运行时不可使用,所以在unity编辑器中提前调用,拿到所有物品;
    [ContextMenu("Fill up item data base")]
    public void FillUpItemDataBase() => _itemDataBase = new List<ItemData>(GetItemDataBase());

    //设置数据库(所有资源),并获取它
    private List<ItemData> GetItemDataBase()
    {
        List<ItemData> _itemDataBase = new List<ItemData>();
        string[] _assetName = AssetDatabase.FindAssets( "", new[] {"Assets/ItemData/Items"} );//返回GUID

        foreach(var SOName in _assetName)
        {
            var SOPath = AssetDatabase.GUIDToAssetPath( SOName );//获取路径
            var itemData = AssetDatabase.LoadAssetAtPath<ItemData>(SOPath);//加载资源
            _itemDataBase.Add(itemData);//添加进数据库
        }
        return _itemDataBase;
    }
#endif

 29.1.图标和光标

文件-?生成设置

生成对应的图标可执行文件 :生成下的CleanBulid

Alt+回车键:切换全屏

30.网络发行

1.生成和构造中:需要安装此模块

2.在unityhu中安装此模块

31.制作新的敌人

在entity脚本添加上RequireComponent:意思为需要的组件,会自动添加组件

using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent (typeof(CapsuleCollider2D))]
[RequireComponent(typeof(EnemyStats))]
[RequireComponent(typeof(EntityFX))]
[RequireComponent((typeof(ItemDrop)))]


public class Enemy : Entity
{}

31.1.slime(史莱姆)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值