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.制作技能树
只能树的分支有两种:
- 等级提升(一条直线)
- 技能分支(多选一)
//技能是否解锁 [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
{}