RPG游戏开发笔录

文章目录

  • RPG游戏开发笔录
    • 1.将普通3D项目升级为RPG渲染管线
    • 2.导入素材(人物,场景,天空盒)
    • 3.第三人称自由视角与移动
    • 4.切换鼠标指针
    • 5.遮挡剔除实现
    • 6.敌人的创建,站岗,追逐
    • 7.人物基本数值实现
    • 8.攻击功能的实现(重难)
    • 9.泛型单例模式以及怪物获胜通知
    • 10.模板生成更多Enemy
    • 11.拓展方法实现怪物的攻击范围限制
    • 12.血条UI的设计
    • 13.玩家升级系统
    • 14.玩家的血条UI
    • 15.传送门(切换关卡)
    • 16.保存数据
    • 17.主菜单的制作
    • 18.场景转场

1.将普通3D项目升级为RPG渲染管线

  • 1.Package Manager 搜索 Universal RP进行安装
  • 2.创建通用渲染管线 Rendering--->URP Assets(with Universal Renderer)
  • 3.进入Project Settings--->Graphics,选中刚创建的渲染管线
  • 4.进入Quality中同样上述操作
  • 5.进入渲染管线,修改Shadows-->Max Distance,以减小渲染时带给显卡的压力
  • 6.修改默认渲染方式为GPU,并可修改GPU类型和烘焙渲染:

2.导入素材(人物,场景,天空盒)

更新渲染:
Windows--->Rendering-->Render Pipeline Converter--->Built-in to URP--->全部勾选--->Initialize Converters--->Convert Assets

3.第三人称自由视角与移动

  • 1.Input Manager–>复制粘贴 Horizontal和Vertical 并修改其为第四,第五坐标轴,命名为 Camera Rate X & Camera Rate Y
  • 2.给摄像机设置父节点 Photographer,使其能绕着父物体移动

相机脚本

public class Photographer : MonoBehaviour
{//相机抬升(绕X轴)public float Pitch { get; private set; }//相机水平角度(绕Y轴)public float Yaw { get; private set; }//鼠标灵敏度public float mouseSensitivity = 5;//摄像机旋转速度public float cameraRotatingSpeed = 80;public float cameraYSpeed = 5;//相机跟随目标private Transform followTarget;public void InitCamera(Transform target){followTarget = target;transform.position = target.position;}private void Update(){UpdateRotation();UpdatePosition();}private void UpdateRotation(){Yaw += Input.GetAxis("Mouse X") * mouseSensitivity;Yaw += Input.GetAxis("CameraRateX") * cameraRotatingSpeed * Time.deltaTime ;//Debug.Log(Yaw);Pitch += Input.GetAxis("Mouse Y") * mouseSensitivity;Pitch += Input.GetAxis("CameraRateY") * cameraRotatingSpeed * Time.deltaTime;//限制抬升角度Pitch = Mathf.Clamp(Pitch, -90, 90);//Debug.Log(Pitch);transform.rotation = Quaternion.Euler(Pitch, Yaw, 0);}private void UpdatePosition(){Vector3 position = followTarget.position;float newY = Mathf.Lerp(transform.position.y, position.y, Time.deltaTime * cameraYSpeed);transform.position = new Vector3(position.x,newY,position.z);}
}

人物移动脚本(通用)

[RequireComponent(typeof(Rigidbody))]
public class CharacterMove : MonoBehaviour
{//刚体组件[Header("组件")]private Rigidbody rb;public Vector3 CurrentInput { get; private set; }public float MaxWalkSpeed = 5;void Awake(){rb = GetComponent<Rigidbody>();}private void FixedUpdate(){rb.MovePosition(rb.position + CurrentInput * MaxWalkSpeed * Time.fixedDeltaTime);//rb.MoveRotation(Quaternion.LookRotation(CurrentInput * MaxWalkSpeed * Time.fixedDeltaTime));//rb.rotation = Quaternion.LookRotation(CurrentInput * MaxWalkSpeed * Time.fixedDeltaTime);//目标旋转角度Quaternion quaternion = Quaternion.LookRotation(CurrentInput * MaxWalkSpeed * Time.fixedDeltaTime);//平滑过渡,deltaTime为每帧渲染的时间transform.localRotation = Quaternion.Lerp(transform.localRotation, quaternion, Time.deltaTime * 10f);}public void SetMovementInput(Vector3 input){CurrentInput = Vector3.ClampMagnitude(input, 1);}

玩家移动脚本

[RequireComponent(typeof(Rigidbody))]
public class PlayerLogic : MonoBehaviour
{[Header("组件")]//刚体组件private Rigidbody rb;//动画组件private Animator anim;//移动组件private CharacterMove characterMove;//摄像机组件public Photographer photographer;//摄像机的根位置public Transform followTarget;//附加项public float jumpForce = 10f;[Header("人物信息")]//人物移动方向private bool isForward, isBack, isLeft, isRight,isFall;void Awake(){rb = GetComponent<Rigidbody>();anim = GetComponent<Animator>();characterMove = GetComponent<CharacterMove>();photographer.InitCamera(followTarget);}void Update(){UpdateMovementInput();//Jump();AttackAnim();SwitchAnim();}//动画变量同步private void SwitchAnim(){anim.SetBool("Forward", isForward);anim.SetBool("Back", isBack);anim.SetBool("Left", isLeft);anim.SetBool("Right", isRight);anim.SetBool("Fall", isFall);}#region 玩家移动//人物移动函数private void UpdateMovementInput(){/** TODO:添加人物死亡条件限制*/float ad = Input.GetAxis("Horizontal");//Debug.Log("ad值为:" + ad);float ws = Input.GetAxis("Vertical");//Debug.Log("ws值为:" + ws);Quaternion rot = Quaternion.Euler(0, photographer.Yaw, 0);characterMove.SetMovementInput(rot * Vector3.forward * ws + rot * Vector3.right * ad);if (ad != 0 || ws != 0){/*//目标旋转角度Quaternion quaternion = Quaternion.LookRotation(new Vector3(ad, 0, ws));//平滑过渡,deltaTime为每帧渲染的时间transform.localRotation = Quaternion.Lerp(transform.localRotation, quaternion, Time.deltaTime * 10f);*///动画状态切换if (ad > 0.1){isRight = true;}else if (ad < -0.1){isLeft = true;}else{isRight = false;isLeft = false;}if (ws > 0.1){isForward = true;}else if (ws < -0.1){isBack = true;}else{isForward = false;isBack = false;}}//添加速度//rb.velocity = new Vector3(ad * moveSpeed, 0, ws * moveSpeed);}#endregion #region 玩家跳跃//跳跃函数private void Jump(){if (Input.GetKeyDown(KeyCode.Space) && isFall){anim.SetTrigger("Jump");rb.velocity = new Vector3(rb.velocity.x, jumpForce,rb.velocity.z);isFall = false;}     }private void OnTriggerEnter(Collider other){if (other.gameObject.CompareTag("Ground")){Debug.Log("触发到地面了");isFall = true;}}#endregion#region 玩家攻击private void AttackAnim(){this.transform.LookAt(this.transform);if (Input.GetMouseButtonDown(0)){anim.SetTrigger("Attack");}else if (Input.GetMouseButtonDown(1)){anim.SetTrigger("Ability");}}#endregion
}

4.切换鼠标指针

切换鼠标指针

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;[RequireComponent(typeof(Rigidbody))]
public class CharacterMove : MonoBehaviour
{    [Header("人物攻击模块")]//鼠标图标public Texture2D target, attack;RaycastHit hitInfo;void Awake(){rb = GetComponent<Rigidbody>();}void Update(){SetCursorTexture();  }//设置鼠标指针void SetCursorTexture(){Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);if (Physics.Raycast(ray,out hitInfo)){switch (hitInfo.collider.gameObject.tag){case "Ground":Cursor.SetCursor(target, new Vector2(16, 16), CursorMode.Auto);break;case "Enemy":Cursor.SetCursor(attack, new Vector2(16, 16), CursorMode.Auto);break;}}}

5.遮挡剔除实现

  • 1.create-->Shader Graph--> URP--->Unit Shader Graph,并起名为 Occlusion Shader
  • 2.基于它创建材质Occlusion放回meterials文件夹
  • 编辑Occlusion Shader为如下参考:

    材质参数参考:

    URP参数参考:

    遮挡剔除实现完成!

6.敌人的创建,站岗,追逐

  • 1.导入素材,设置基本状态信息(Idle,Chase,Guard,Dead)
  • 2.敌人的状态切换,追逐玩家以及切换动画:
using UnityEngine;
using UnityEngine.AI;public enum EnemyStates { GUARD,PATROL,CHASE,DEAD }[RequireComponent(typeof(NavMeshAgent))]
public class EnemyController : MonoBehaviour
{[Header("组件")]private NavMeshAgent agent;private Animator anim;[Header("状态变量")]private EnemyStates enemyStates;[Header("敌人基础设置")]//敌人可视范围public float sightRadius;//敌人的类型public bool isGuard;//敌人攻击目标private GameObject AttackTarget;//记录敌人初始站岗位置private Vector3 GuardPos;//敌人状态bool isWalk,isDead;//脱战观望时间public float remainLookatTime = 3f;//脱战停留计时器private float remainTimer;private void Awake(){agent = GetComponent<NavMeshAgent>();anim = GetComponent<Animator>();}void Start(){//给敌人初始位置赋值GuardPos = transform.position;}void Update(){SwitchStates();SwitchAnimation();}//切换动画void SwitchAnimation(){anim.SetBool("Chase", isChase);anim.SetBool("Walk", isWalk);anim.SetBool("Dead", isDead);}void SwitchStates(){//如果发现玩家,切换到Chaseif (FoundPlayer()){enemyStates = EnemyStates.CHASE;Debug.Log("发现玩家");}switch (enemyStates){case EnemyStates.GUARD:Guard();break;case EnemyStates.PATROL:break;case EnemyStates.CHASE:ChasePlayer();break;case EnemyStates.DEAD:break;}}//是否发现玩家bool FoundPlayer(){//拿到检测到对应范围内的所以碰撞体var hitColliders = Physics.OverlapSphere(this.transform.position, sightRadius);//检测其中是否存在Playerforeach(var target in hitColliders){if (target.gameObject.CompareTag("Player")){AttackTarget = target.gameObject;return true;}}//脱离目标AttackTarget = null;return false;}//追逐玩家函数void ChasePlayer(){//脱战逻辑if (!FoundPlayer()){if (remainTimer > 0){//观望战立状态agent.destination = transform.position;remainTimer -= Time.deltaTime;isWalk = false;}else{//回到上一个状态(巡逻或者站立)enemyStates = isGuard ? EnemyStates.GUARD : EnemyStates.PATROL;}}else{agent.isStopped = false;isWalk = true;//刷新观望时间计时器remainTimer = remainLookatTime;//当距离小于攻击距离时,开始攻击if (Vector3.Distance(transform.position, AttackTarget.transform.position) < characterStats.AttackData.attackRange){isWalk = false;agent.isStopped = true;anim.SetTrigger("Attack");}else{//追击Playeragent.destination = AttackTarget.transform.position;}}}//怪物返回出生点以及站岗逻辑void Guard(){//判断怪物是否返回出生点if (Vector3.Distance(transform.position,GuardPos) <= 3){isWalk = false;}else{isWalk = true;agent.destination = GuardPos;}}
}

7.人物基本数值实现

  • 1.给文件夹分好类,分别创建 MonoBehaviorScriptableObject文件夹。
  • 2.创建第一个ScriptableObject脚本文件,命名为 CharacterData_SO
  • 3.编写人物应有的通用属性,并在专门的数据文件夹下创建出数据文件。

数据模板写法

[CreateAssetMenu(fileName ="New Data",menuName = "Character Stats/Data")]
public class CharacterData_SO : ScriptableObject
{[Header("Stats Info")]//最大生命值public int maxHealth;//当前生命值public int currentHealth;//基础防御力public int baseDefence;//当前防御力public int currentDefence;
}

数据操作写法

public class CharacterStats : MonoBehaviour
{public CharacterData_SO characterData;#region Read from Data_SOpublic int MaxHealth {get{return characterData != null ? characterData.maxHealth : 0;}set{characterData.maxHealth = value;}}public int CurrentHealth{get{return characterData != null ? characterData.currentHealth : 0;}set{characterData.currentHealth = value;}}public int BaseDefence{get{return characterData != null ? characterData.baseDefence : 0;}set{characterData.baseDefence = value;}}public int CurrentDefence{get{return characterData != null ? characterData.currentDefence : 0;}set{characterData.currentDefence = value;}}#endregion
}
  • 4.将数据操作脚本 CharacterStats挂载到Player和Enemy上,并拖入对应数据文件。
  • 5.在玩家控制脚本中获取到数据文件,并获取到操作该数据的方法。
  • 6.同样方式实现攻击数值的基本书写:
[CreateAssetMenu(fileName = "New Attack",menuName = "Attack/Data")]
public class AttackData_SO : ScriptableObject
{//攻击距离public float attackRange;//技能距离public float skillRange;//冷却时间public float coolDown;//基础伤害范围public int minDamage;public int maxDamage;//暴击倍率(伤害)public float criticalMultiplier;//暴击率public float criticalChance;
}

8.攻击功能的实现(重难)

  • 1.创建攻击数据脚本:包括玩家和怪物
    给出参考:Player Attack Data_SO
using System.Collections;
using System.Collections.Generic;
using UnityEngine;[CreateAssetMenu(fileName = "New Attack",menuName = "Attack/Data")]
public class AttackData_SO : ScriptableObject
{//攻击距离public float attackRange;//技能距离public float skillRange;//冷却时间public float coolDown;//基础伤害范围public int minDamage;public int maxDamage;//暴击倍率(伤害)public float criticalMultiplier;//暴击率public float criticalChance;
}

玩家攻击脚本面板参考:

  • 2.在角色数据操作脚本中引入攻击脚本:并写出伤害计算逻辑
public class CharacterStats : MonoBehaviour
{public CharacterData_SO characterData;public AttackData_SO AttackData;#region 攻击模块public void TakeDamage(CharacterStats attacker,CharacterStats defencer){//计算一次攻击的伤害float coreDamage = UnityEngine.Random.Range(attacker.AttackData.minDamage, attacker.AttackData.maxDamage);//控制最低伤害为1int damage = Mathf.Max((int)coreDamage - defencer.CurrentDefence,1);//控制血量最低为0CurrentHealth = Mathf.Max(CurrentHealth - damage, 0);//更新 UI}#endregion
}

由于这里攻击函数是玩家和怪物通用的,所以后续只需要传入攻击者和受击者即可正常完成伤害计算并扣血。其次,怪物的进攻逻辑是AI操控,只需要在追踪玩家时,吧玩家对象传入怪物的攻击目标变量即可。但玩家的攻击目标选择却成了问题,这里采用类似怪物发现玩家的方式,采用一个圆形探测范围(该范围阈值即为玩家的攻击范围),当玩家进行攻击操作时,只需要检测怪物是否在范围检测距离内,如果在距离内,则正常调用TakeDamage(),如果不在,则视为空刀。缺点是实际攻击范围(以玩家为圆心,以攻击距离为半径的圆形内)与动画攻击范围(人物的前方扇形区域)不符合。后续有待改进…此外,暴击功能较为繁琐,目前未实现。以下为判断暴击的常用方法之一,仅供参考:

//设置全局变量,判断是否暴击
characterStats.isCritical = UnityEngine.Random.value < characterStats.attackData.criticalChance;
  • 3.怪物的攻击逻辑:(将其挂载到怪物攻击动画的某一帧上)
 //引入数值组件private CharacterStats characterStats;//给组件赋值private void Awake(){characterStats = GetComponent<CharacterStats>();}//每次游戏开始给怪物重置生命值void Start(){//刷新初始生命值characterStats.CurrentHealth = characterStats.MaxHealth;}//怪物攻击的出伤害事件void Hit(){if (AttackTarget != null){//拿到受害者的属性信息var targetStats = AttackTarget.GetComponent<CharacterStats>();//造成伤害targetStats.TakeDamage(characterStats, targetStats);}}
  • 4.玩家的攻击逻辑
//引入数值组件
//Awake初始化数值组件
//初始化生命值#region 玩家攻击public void AttackAnim(){if (Input.GetMouseButtonDown(0)){anim.SetTrigger("Attack");}else if (Input.GetMouseButtonDown(1)){anim.SetTrigger("Ability");}}//范围检测private bool AttackRangeTest(){Debug.Log(characterStats.AttackData.attackRange);//拿到检测到对应范围内的所有碰撞体Collider[] hitColliders = Physics.OverlapSphere(transform.position, characterStats.AttackData.attackRange);foreach (Collider collider in hitColliders){if (collider.gameObject.CompareTag("Enemy")){AttackTarget = collider.gameObject;return true;}}return false;}//玩家的出伤害逻辑void Hit(){if (AttackRangeTest()){CharacterStats targetStats = AttackTarget.GetComponent<CharacterStats>();targetStats.TakeDamage(characterStats, targetStats);}else{Debug.Log("空刀!");}}
#endregion
  • 5.死亡的判断
    思路:死亡判断直接在Update()函数中逐帧判断当前生命值是否为0即可:
    玩家死亡:
//引入变量bool isDead;void Update(){if (characterStats.CurrentHealth != 0){UpdateMovementInput();Jump();AttackAnim();}else{isDead = true;rb.constraints = RigidbodyConstraints.FreezePosition;rb.constraints = RigidbodyConstraints.FreezeRotation;}SwitchAnim();}

怪物死亡:

 void Update(){if (characterStats.CurrentHealth != 0){SwitchStates();}else{isDead = true;enemyStates = EnemyStates.DEAD;}SwitchAnimation();}

9.泛型单例模式以及怪物获胜通知

  • 1.写一个管理单例模式的基类( Tools/Singleton ):
//管理类的基类
//泛型单例模式public class Singleton<T> : MonoBehaviour where T:Singleton<T>
{private static T instance;//外部可访问的函数public static T Instance{get { return instance; }}//可继承可重写的Awake方法protected virtual void Awake(){if (instance != null){Destroy(gameObject);}else{instance = (T)this;}}public static bool IsInitialized{get { return instance != null; }}protected virtual void OnDestroy() { if (instance == this){instance = null;}    }
}
  • 2.写一个发放广播的接口 IEndGameObserver
public interface IEndGameObserver
{void EndNotify();
}
  • 3.写一个游戏管理类继承管理基类 ( Manages/GameManager )
public class GameManager : Singleton<GameManager>
{public CharacterStats playerStats;//收集所有需要接收广播的对象List<IEndGameObserver> endGameObservers = new List<IEndGameObserver>();//在游戏开始时将玩家信息注册到管理类public void RigisterPlayer(CharacterStats player){playerStats = player;}//在游戏开始时,将怪物信息(被通知者)录入通知对象public void AddObserver(IEndGameObserver observer){endGameObservers.Add(observer);}//怪物被消灭时,清理通知对象public void RemoveObserver(IEndGameObserver observer){endGameObservers.Remove(observer);}//通知逻辑public void NotifyObservers(){foreach(var observer in endGameObservers){observer.EndNotify();}}
}
  • 4.在玩家逻辑中添加注册信息到管理类
    PlayerLogic:
void Start(){//利用单例模式调用注册玩家信息GameManager.Instance.RigisterPlayer(characterStats);}
  • 5.在怪物逻辑中添加 注册和销毁通知,以及通知内容的具体实现
    EnemyController:
//首先让其实现IEndGameObserver接口
public class EnemyController : MonoBehaviour,IEndGameObserver
{private void OnEnable(){GameManager.Instance.AddObserver(this);}private void OnDisable(){if (!GameManager.IsInitialized) return;GameManager.Instance.RemoveObserver(this);}
}public void EndNotify(){//获胜动画//停止所有移动//停止AgentisWalk = false;AttackTarget = null;isVictory = true;}

报错总结:此处在GameManager的实例化阶段一直报 空引用异常,后来发现自己没有创建 Game Manager 的结点,并挂载GameManager的脚本。此外,还需注意 Onable函数是场景初始化时被创建,此处不涉及场景切换,故,需将其暂时写在Start方法中,后续再更改。

10.模板生成更多Enemy

  • 1.由于之前写法会使多个复制出来的怪物属性共享,这显然不是我们需要的功能,故我们需要创建一个属性模板,让其新生成的怪物以该模板生成对应的数值。
    Character Stats中:
public class CharacterStats : MonoBehaviour
{public CharacterData_SO templateData;public CharacterData_SO characterData;void Awake(){if (templateData != null){characterData = Instantiate(templateData);}    }
}
  • 2.引入新怪物的美术模型资源,添加必要组件EnemyController,NavMesh Agent以及基本碰撞体,别忘了将其标签改为Enemy,并创建对应的数据文件。
  • 3.在重置新类型敌人的动画信息时,可直接创建一个 Override Animators,可十分便利的重置怪物动画。

11.拓展方法实现怪物的攻击范围限制

由于之前只要怪物发动攻击动画,玩家必掉血。这是极度不合理的,我们希望玩家有一定的闪避空间或容错,故我们采用 拓展方法 Extension Method 来对其攻击范围做一个限制。

  • 1.写一个 Extension Method脚本:
public static class ExtensionMethod
{//确保怪物攻击在正前方[-60°,60°]之间范围攻击有效private const float dotThreshold = 0.5f;public static bool IsFacingTarget(this Transform transform, Transform target){//计算出目标物对于攻击者正前方的相对位置并取单位向量var vectorToTarget = target.position - transform.position;vectorToTarget.Normalize();float dot = Vector3.Dot(transform.forward, vectorToTarget);return dot >= dotThreshold;}
}
  • 2.并在怪物的攻击动画事件中加上限制条件:
    EnemyController:
//怪物攻击的出伤害事件void Hit(){if (AttackTarget != null && transform.IsFacingTarget(AttackTarget.transform)){//拿到受害者的属性信息var targetStats = AttackTarget.GetComponent<CharacterStats>();//造成伤害targetStats.TakeDamage(characterStats, targetStats);}}
  • 3.给Boss的基础攻击增加击退效果

但暂时有点问题,无法正常击退玩家。后续再改BUG。

public class Boss_Rock : EnemyController
{//击飞玩家的力public float kickForce = 25f;public void KickOff(){if (AttackTarget != null && transform.IsFacingTarget(AttackTarget.transform)){Debug.Log("被踢开了");//拿到受害者的属性信息var targetStats = AttackTarget.GetComponent<CharacterStats>();//计算击飞方向(问题)//FIXME:有待修改Vector3 direction = (AttackTarget.transform.position - transform.position).normalized;AttackTarget.GetComponent<Rigidbody>().velocity = direction * kickForce;//造成伤害targetStats.TakeDamage(characterStats, targetStats);}}
}
  • 4.设置石头人可投掷物

– 1.拖入石头的素材,添加必要组件:RigidBody,Mesh Collider(勾选第一项),脚本Rock.cs

public class Rock : MonoBehaviour
{private Rigidbody rb;[Header("Basic Settings")]public float force;public GameObject target;private Vector3 direction;void Start(){rb = GetComponent<Rigidbody>();FlyToTarget();   }public void FlyToTarget(){//预防石头生成瞬间玩家脱离范围if (target == null){target = FindObjectOfType<PlayerController>().gameObject;}direction = (target.transform.position - transform.position + Vector3.up).normalized;rb.AddForce(direction * force, ForceMode.Impulse);}
}

– 2.修改石头人生成石头代码:

public class Boss_Rock : EnemyController
{//扔出的石头的预制体public GameObject rockPrefab;//出手点的坐标public Transform handPos;//投掷石头的逻辑public void ThrowRock(){if (AttackTarget != null){var rock = Instantiate(rockPrefab,handPos.position,Quaternion.identify);rock.GetComponent<Rock>().target = AttackTarget;}}
}

– 3.将ThrowRock方法添加到动画对应帧数上。

– 4.设置石头的状态:在被投掷出的时候能对敌人以及玩家造成伤害,但落地以后无法对玩家或敌人造成伤害。

public class Rock : MonoBehaviour
{public enum RockStates { HitPlayer,HitEnemy,HitNothing };//石头的伤害值public int damage = 8;//石头的状态public RockStates rockStates;void Start(){rb = GetComponent<Rigidbody>();//为了防止石头刚一出来就被判断为hitNothingrb.velocity = Vector3.one;//初始化石头的状态rockStates = RockStates.HitPlayer;//石头被生成的时候就自动飞向目标FlyToTarget();//石头扔出三秒后延迟销毁Destroy(this.gameObject, 3);}//逐帧判断,当石头几乎静止时变得不再有威胁void FixedUpdate(){Debug.Log(rb.velocity.sqrMagnitude);if (rb.velocity.sqrMagnitude < 1){rockStates = RockStates.HitNothing;}    }void OnCollisionEnter(Collision collision){switch (rockStates){case RockStates.HitPlayer:if (collision.gameObject.CompareTag("Player")){CharacterStats characterStats = collision.gameObject.GetComponent<CharacterStats>();//碰到玩家了,造成伤害,并对玩家播放受击动画(TakeDamage的函数重载)characterStats.TakeDamage(damage,characterStats);collision.gameObject.GetComponent<Animator>().SetTrigger("Hit");rockStates = RockStates.HitNothing;}break;case RockStates.HitEnemy:if (collision.gameObject.CompareTag("Enemy")){var EnemyStats = collision.gameObject.GetComponent<CharacterStats>();EnemyStats.TakeDamage(damage, EnemyStats);//攻击到敌人以后,也将其设为无危胁状态}break;}    }
}

– 5.函数重载TakeDamage()方法,让石头也能造成伤害:

 public void TakeDamage(int damage,CharacterStats defencer){int finalDamage = Mathf.Max(damage - defencer.CurrentDefence, 1);CurrentHealth = Mathf.Max(CurrentHealth - finalDamage, 0);}

– 6.修改玩家的攻击逻辑,使其攻击石头也具有一定逻辑:

//范围检测private bool AttackRangeTest(){//Debug.Log(characterStats.AttackData.attackRange);//拿到检测到对应范围内的所有碰撞体Collider[] hitColliders = Physics.OverlapSphere(transform.position, characterStats.AttackData.attackRange);foreach (Collider collider in hitColliders){//有限判断敌人,如果不存在敌人,则判断是否有可攻击物if (collider.gameObject.CompareTag("Enemy") || collider.gameObject.CompareTag("Attackable")){AttackTarget = collider.gameObject;return true;}}return false;}//玩家的出伤害逻辑void Hit(){if (AttackRangeTest()){if (AttackTarget.CompareTag("Attackable")){//进一步判断是石头if (AttackTarget.GetComponent<Rock>()){AttackTarget.GetComponent<Rock>().rockStates = Rock.RockStates.HitEnemy;AttackTarget.GetComponent<Rigidbody>().velocity = Vector3.one;AttackTarget.GetComponent<Rigidbody>().AddForce(transform.forward * 20, ForceMode.Impulse);}}else if (AttackTarget.CompareTag("Enemy")){CharacterStats targetStats = AttackTarget.GetComponent<CharacterStats>();targetStats.TakeDamage(characterStats, targetStats);}}}

12.血条UI的设计

  • 1.创建一个Canvas命名为 HealthBarCanvas,修改Canvas的 UI Scale Mode改为 World Space,并设置相机Camera。创建一个子物体UI image,命名 Bar Holder。

  • 2.对UI界面的位置信息进行调整,修改长3和高0.25(参考值)。

  • 3.在Package Manager中引入 3D sprite,创建一个2D Object–>Square,找到其基础的文件,复制一份图片另存起来。将其拖入到Bar Holder的Source Image中,并可修改其颜色(血条底色)。

  • 4.继续创建Bar Health的子节点Image,尺寸参数与父节点保持一致,拖入Source Image,修改颜色(血条上层色),并将其改为滑动条的形式进行显示。

  • 5.写UI脚本操控血条的变化。

public class HealthBarUI : MonoBehaviour
{//血条预制体public GameObject healthBarPrefab;//血条位置public Transform barPoint;//是否让血条持续显示public bool alwaysVisible;//血条被唤醒后显示的时间public float visibleTime;//血量滑动条Image healthSlider;Transform UIBar;//摄像机的位置(保证始终正对摄像机)Transform camera;//拿到当前目标的血量信息private CharacterStats currentStats;void Start(){currentStats = GetComponent<CharacterStats>();}void OnEnable(){camera = Camera.main.transform;foreach (Canvas canvas in FindObjectsOfType<Canvas>()){if (canvas.renderMode == RenderMode.WorldSpace){UIBar = Instantiate(healthBarPrefab, canvas.transform).transform;healthSlider = UIBar.GetChild(0).GetComponent<Image>();UIBar.gameObject.SetActive(alwaysVisible);}}}//血条跟随敌人void LateUpdate(){if (UIBar != null){UIBar.position = barPoint.position;UIBar.forward = -camera.forward;}}void Update(){UpdateHealthBar(currentStats.CurrentHealth, currentStats.MaxHealth);}private void UpdateHealthBar(int currentHealth, int maxHealth) {if (currentHealth <= 0){if (UIBar != null){Destroy(UIBar.gameObject);}}else{UIBar.gameObject.SetActive(true);float sliderPercent = (float)currentHealth / maxHealth;//Debug.Log("当前血量仅剩:" + sliderPercent);healthSlider.fillAmount = sliderPercent;}}}

总结:该套程序采用逐帧检测血条变化,相对来说效率较差,优化可以让角色攻击时计算一次血条变化。后续会优化!!!

13.玩家升级系统

  • 1.先添加一下玩家的属性
    Character_SO:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;[CreateAssetMenu(fileName ="New Data",menuName = "Character Stats/Data")]
public class CharacterData_SO : ScriptableObject
{[Header("Stats Info")]//基础生命值public float baseHealth;//最大生命值public float maxHealth;//当前生命值public float currentHealth;//基础防御力public int baseDefence;//当前防御力public int currentDefence;[Header("击杀敌人获得的经验值")]public int killExp;[Header("玩家升级系统")]//当前等级public int currentLevel;//最大等级public int maxLevel;//升级所需基础经验值public int baseExp;//当前经验值public int currentExp;//升级经验提高值public int ExpIncrement;//升级属性加权public float levelBuff;//更新经验值函数public void UpdateExp(int Exp){currentExp += Exp;if (currentExp >= baseExp){LevelUp();}}private void LevelUp(){//提高等级(将当前等级限制在[0,maxLevel之间])currentLevel = Mathf.Clamp(currentLevel + 1, 0, maxLevel);//升级所需的经验值也随之提高baseExp += ExpIncrement;//血量提高5%maxHealth = baseHealth * (levelBuff * currentLevel + 1);currentHealth = maxHealth;Debug.Log("LevelUP,当前血量为" + currentHealth);}
}
  • 暂时为了学习升级逻辑的构思以及数值的模范书写,后续功能有待优化!

  • 2.在造成伤害界面 添加击杀增加经验的判断:
    CharacterStats:

 public void TakeDamage(CharacterStats attacker,CharacterStats defencer){//计算一次攻击的伤害float coreDamage = UnityEngine.Random.Range(attacker.AttackData.minDamage, attacker.AttackData.maxDamage);//控制最低伤害为1int damage = Mathf.Max((int)coreDamage - defencer.CurrentDefence,1);//控制血量最低为0CurrentHealth = Mathf.Max(CurrentHealth - damage, 0);//更新 UI//经验值updateif (CurrentHealth == 0){attacker.characterData.UpdateExp(defencer.characterData.killExp);}}

14.玩家的血条UI

  • 1.同样优先创建一个Canvas,设置参考如下:

做出如下效果的UI界面:

并在其父节点下嵌入以下脚本以控制血条的变化:
PlayerHealthUI:

using UnityEngine.UI;public class PlayerHealthUI : MonoBehaviour
{Text levelText;Image healthSlider;Image expSlider;void Awake(){//拿到文本子节点levelText = transform.GetChild(2).GetComponent<Text>();healthSlider = transform.GetChild(0).GetChild(0).GetComponent<Image>();expSlider = transform.GetChild(1).GetChild(0).GetComponent<Image>();}void Update(){levelText.text = "level " + GameManager.Instance.playerStats.characterData.currentLevel.ToString("00");UpdateHealth();UpdateExp();}void UpdateHealth(){float sliderPercent = (float)GameManager.Instance.playerStats.CurrentHealth / GameManager.Instance.playerStats.MaxHealth;healthSlider.fillAmount = sliderPercent;}void UpdateExp(){float sliderPercent = (float)GameManager.Instance.playerStats.characterData.currentExp / GameManager.Instance.playerStats.characterData.baseExp;expSlider.fillAmount = sliderPercent;}
}

15.传送门(切换关卡)

场景内传送

  • 1.在之前Shader Graph目录下继续创建Shader,参数参考如下:
  • 2.利用创建的Shader的基础上创建一个Meterial,并调参数。
  • 3.在层级窗口中创建一个Quad,并添加以上材质,在其下创建一个子节点 DestinationPoint 作为被传送点。
  • 4.创建 TransitionPoint.cs脚本控制传送点:(挂载在传送门父类上)
public class TransitionPoint : MonoBehaviour
{//传送类型(同场景,不同场景)public enum TransitionType{SameScene,DifferentScene}[Header("info")]//场景名字(如果同场景则可以不填)public string sceneName;//传送类型public TransitionType type;//传送到的目的地点public TransitionDestination.DestinationTags destinationTag;//只有该属性触发了才会传送private bool canTrans;void Update(){if (Input.GetKeyDown(KeyCode.F) && canTrans){//场景传送SceneController.Instance.TransitionToDestination(this);}    }void OnTriggerStay(Collider other){if (other.CompareTag("Player")){canTrans = true;}}void OnTriggerExit(Collider other){if (other.CompareTag("Player")){canTrans = false ;}}
}
  • 5.创建传送目标点脚本 TransitionDestination.cs:
public class TransitionDestination : MonoBehaviour
{public enum DestinationTags{ENTRE,A,B,C}public DestinationTags destinationTag;
}
  • 6.写场景控制脚本实现同场景传送逻辑:SceneController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;public class SceneController : Singleton<SceneController>
{Transform player;//玩家的预制体(加载新场景时引入)public GameObject playerPrefab;protected override void Awake(){base.Awake();DontDestroyOnLoad(this);}public void TransitionToDestination(TransitionPoint transitionPoint){switch (transitionPoint.type) {case TransitionPoint.TransitionType.SameScene:StartCoroutine(Transition(SceneManager.GetActiveScene().name, transitionPoint.destinationTag));break;case TransitionPoint.TransitionType.DifferentScene:StartCoroutine(Transition(transitionPoint.sceneName, transitionPoint.destinationTag));break;}}//使用携程异步加载场景IEnumerator Transition(string sceneName , TransitionDestination.DestinationTags destinationTag){//TODO:保存数据//判断是相同场景还是不同场景if (SceneManager.GetActiveScene().name != sceneName){//等待return值完成后再执行其他代码(异步加载)yield return SceneManager.LoadSceneAsync(sceneName);yield return Instantiate(playerPrefab, GetDestination(destinationTag).transform.position,GetDestination(destinationTag).transform.rotation);//中断携程yield break;}else{player = GameManager.Instance.playerStats.transform;player.SetPositionAndRotation(GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);yield return null;}     }//在全部传送门中查找传送标签一样的传送门private TransitionDestination GetDestination(TransitionDestination.DestinationTags destinationTag){var entrances = Resources.FindObjectsOfTypeAll<TransitionDestination>();for (int i = 0; i < entrances.Length; i++){if (entrances[i].destinationTag == destinationTag){return entrances[i];}}return null;}
}

以上代码遇到的问题:

  • 1.切换到新场景后,管理类代码全部消失,导致游戏无法正常运行,解决方法:在Manage相关代码前都加上重写的Awake()方法即可:
protected override void Awake()
{base.Awake();//意为该Manage脚本不会被销毁DontDestroyOnLoad(this);
}
  • 2.注意在切换至新场景之前,一定要在新场景安置 自己写的 PhotoGrapher 结点,并且在PlayerLogic初始化的时候找到摄像机别赋值完整:
 void Awake(){photographer = FindObjectOfType<Photographer>();rb = GetComponent<Rigidbody>();anim = GetComponent<Animator>();characterMove = GetComponent<CharacterMove>();photographer.InitCamera(followTarget);characterStats = GetComponent<CharacterStats>();audio = photographer.GetComponent<AudioSource>();}
  • 3.这样改完以后从第二场景重新返回第一场景时,会出现两个人物,并且人物在半空中。 (埋个伏笔)

16.保存数据

  • 1.使用JSON保存游戏数据,在切换场景时调用保存函数与读取函数:
  • 2.新建一个结点 SaveManager并创建脚本 SaveManager.cs挂载到结点上:
public class SaveManager : Singleton<SaveManager>
{protected override void Awake(){base.Awake();DontDestroyOnLoad(this);}//封装保存和读取玩家信息的函数public void SavePlayerData(){Save(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);}public void LoadPlayerData(){Load(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);}//存储数据public void Save(Object data,string key){//将要存储的数值转化为JSONvar jsonData = JsonUtility.ToJson(data,true);//将数据以键值对的形式保存PlayerPrefs.SetString(key, jsonData);PlayerPrefs.Save();}//加载数据public void Load(Object data, string key){if (PlayerPrefs.HasKey(key)){JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString(key), data);}}
}
  • 3.在切换场景时调用这个两个函数即可完成对玩家数据的保存:
    SceneController.cs
//使用携程异步加载场景IEnumerator Transition(string sceneName , TransitionDestination.DestinationTags destinationTag){//TODO:保存数据SaveManager.Instance.SavePlayerData();//判断是相同场景还是不同场景if (SceneManager.GetActiveScene().name != sceneName){//等待return值完成后再执行其他代码(异步加载)yield return SceneManager.LoadSceneAsync(sceneName);yield return Instantiate(playerPrefab, GetDestination(destinationTag).transform.position,GetDestination(destinationTag).transform.rotation);//读取数据SaveManager.Instance.LoadPlayerData();//中断携程yield break;}else{player = GameManager.Instance.playerStats.transform;player.SetPositionAndRotation(GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);yield return null;}     }
  • 4.切记勿忘在新场景创建角色UI。

17.主菜单的制作

  • 1.制作UI,按钮,标题,背景等等
  • 2.分别实现退出游戏,新的游戏和继续游戏的脚本功能:
    – 1.退出游戏
public class MainManu : MonoBehaviour
{Button quitBtn;void Awake(){quitBtn = transform.GetChild(3).GetComponent<Button>();quitBtn.onClick.AddListener(QuitGame);}void QuitGame(){Application.Quit();Debug.Log("退出游戏");}
}

– 2.新的游戏:
1).清除之前的数据
2).在游戏控制中添加寻找全图起点的方法:
GameManager.cs:

 //寻找入口的函数public Transform GetEntrance(){foreach (var item in FindObjectsOfType<TransitionDestination>()){if (item.destinationTag == TransitionDestination.DestinationTags.ENTRE){return item.transform;}}return null;}

2).切换到第一场景的某点(在场景控制中使用携程切换场景)

public void TransitionToFirstLevel(){StartCoroutine(LoadLevel("City"));}IEnumerator LoadLevel(string scene){if (scene != ""){yield return SceneManager.LoadSceneAsync(scene);yield return player = Instantiate(playerPrefab, GameManager.Instance.GetEntrance().position, GameManager.Instance.GetEntrance().rotation).transform;//保存数据SaveManager.Instance.SavePlayerData();yield break;}}

– 3.继续游戏的实现:
1).在保存函数里加入保存当前地图逻辑

public class SaveManager : Singleton<SaveManager>
{//当前所在的场景private string sceneName = "";public string SneceName { get { return PlayerPrefs.GetString(sceneName); } }protected override void Awake(){base.Awake();DontDestroyOnLoad(this);}//封装保存和读取玩家信息的函数public void SavePlayerData(){Save(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);}public void LoadPlayerData(){Load(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);}//存储数据public void Save(Object data,string key){//将要存储的数值转化为JSONvar jsonData = JsonUtility.ToJson(data,true);//将数据以键值对的形式保存PlayerPrefs.SetString(key, jsonData);PlayerPrefs.SetString(sceneName, SceneManager.GetActiveScene().name);PlayerPrefs.Save();}//加载数据public void Load(Object data, string key){if (PlayerPrefs.HasKey(key)){JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString(key), data);}}
}

2).在场景控制里,调用协程:
SceneController.cs

public void TransitionToLoadGame(){StartCoroutine(LoadLevel(SaveManager.Instance.SceneName));}

3).在玩家生成的生成的时候,就读取一遍玩家数据
PlayerController.cs

 void OnEnable(){//利用单例模式调用注册玩家信息GameManager.Instance.RigisterPlayer(characterStats);}void Start(){SaveManager.Instance.LoadPlayerData();//初始化攻击冷却计时器(初始状态可攻击)AttackCD = -1;}

4).给予玩家回到Main的方式:
SceneController.cs:

 public void TransitionToMain(){StartCoroutine(LoadMain());}IEnumerator LoadMain(){yield return SceneManager.LoadSceneAsync("Main");yield break;}

5).添加补全继续游戏的调用函数
MainMenu.cs:

 void ContinueGame(){//读取进度SceneController.Instance.TransitionToMain();}

18.场景转场

  • 1.引入TimeLine窗口:Windows-->Sequencing-->Timeline

基于Unity引擎的RPG3D项目开发笔录相关推荐

  1. 基于Unity引擎利用OpenCV和MediaPipe的面部表情和人体运动捕捉系统

    基于Unity引擎利用OpenCV和MediaPipe的面部表情和人体运动捕捉系统 前言 项目概述 项目实现效果 2D面部表情实时捕捉 3D人体动作实时捕捉 补充 引用 前言 之前做的一个项目--使用 ...

  2. 如何用 Python 进行基于深度学习的计算机视觉项目开发?

    令人惊喜的"智能"年代 深度学习有着广阔的前景 我们正处在一个"智能"的年代,比如智能手机中的语音助手.机器翻译和人脸识别:战胜过日本将棋冠军.国际象棋冠军, ...

  3. hibernate 读取mysql表结构_为什么要用hibernate 与基于数据库表结构的项目开发

    最近开始学习hibernate,其实并不知道要学习什么,有什么用.后来问了一下同事,他就说快捷方便简单,很多事情不用自己做他会帮你做好,但是我觉得不应该是这样的, 于是我就去搜了一下,就搜到了一篇帖子 ...

  4. 新书推荐 |《基于区块链的物联网项目开发》

    新书推荐 <基于区块链的物联网项目开发> 点击上图了解及购买 探索物联网架构中分类记账技术的实际实现,研究智能设备的安全最佳实践,了解端到端物联网解决方案的区块链实现. 编辑推荐 将物联网 ...

  5. 基于SpringBoot和MyBatisPlus的项目开发脚手架

    本博客主要功能是从创建Spring Boot项目开始讲解如何搭建一个较为通用的快速开发脚手架,方便在以后的开发中可以快速的应用,避免每次写都要去以前的项目里翻工具类和通用配置. 代码下载地址:基于Sp ...

  6. AngryTask - 基于伪 scrum 的个人项目开发产品

    关于 去年年末的时候同事分享了一下 scrum 工作模型, 以后公司按照这种方式来执行产品开发. 联想自己在阿里的两年的工作方式和大学课程讲述的项目协同敏捷开发的一些知识. 所以本文想就开发工作流模型 ...

  7. 基于unity+高通AR项目的一些总结

    今天,公司做的第一款AR项目终于在苹果appstore上架了.将近三个多月的踩坑和摸索也终于告一段落了,接下来就是不断的进行版本优化和更新,这将是一个漫长的过程.在此,对自己三个多月的开发做一个阶段性 ...

  8. 从零开始制作基于Unity引擎的宝石消消乐(一)

    完整项目我已经放到GitHub啦~ GitHub: https://github.com/lucaschen1993/Lukastar 市场上有些消消乐真好玩,比如hxxxxxpop,pxxxxsag ...

  9. 基于Unity引擎的2D像素风Roguelike地下城游戏Demo

    文章目录 前言 一.场景搭建 1.基础房间 2.随机房间的生成 3.门与墙的生成 4.小地图 5.摄像机在房间之间的转移 二.人物制作 1.基础设定 2.人物动画 3.基础移动 4.攻击动作 三.战斗 ...

  10. 从零开始制作基于Unity引擎的宝石消消乐(二)

    完整项目我已经放到GitHub啦~ GitHub: https://github.com/lucaschen1993/Lukastar 前言 上一篇文章说到游戏的设计,现在就来讲一讲游戏内操作方法的实 ...

最新文章

  1. 何恺明团队新作!深度学习网络架构新视角:通过相关图表达理解神经网络
  2. idea建立一个java工程_IntelliJ IDEA(三、各种工程的创建 -- 之一 -- 创建一个简单的Java工程)...
  3. 4.3.2 基于集合的操作
  4. 使用ReportNG更好看的TestNG HTML测试报告– Maven指南
  5. linux在指定目录多个文件中搜索关键字
  6. 新一代需求管理工具Trufun Bacon X正式发布!
  7. 虚实结合:无需人工标注的可泛化行人再辨识
  8. Linux概要端口,LINUX中如何查看某个端口是否被占用(转发)
  9. Halcon基于形状的几何定位函数说明
  10. 拓扑排序---AOV图
  11. 调和分析笔记1|极大函数法及简单应用
  12. 【mybatis】mybatis基础知识总结
  13. 51CTO的企业文化——水文化
  14. linux最小化连接开放wifi,CentOS最小化安装后启用无线连接网络
  15. ZOJ 1060 Count the Color
  16. 什么是AppleSpell,为什么它可以在Mac上运行?
  17. 创业4年女掌门刘静瑜,创造动力电池,中创新航市值超600亿
  18. 麻省理工学院公开课:单变量微积分
  19. android 根据屏幕大小自行选择图片
  20. linux uvc 拍照程序,Linux UVC driver

热门文章

  1. 辐射校正(传感器定标+大气校正)
  2. 下载sqlserver2012 试用_大肥虫助手app下载-大肥虫助手最新版本下载v7.0.4
  3. CenterCrop的Video View
  4. 按ASCII码给json对象排序
  5. Cadence Allegro编辑元件属性图文教程及视频演示
  6. 四阶五级matlab,微分方程数值解法matlab(四阶龙格—库塔法).ppt
  7. 二(高)阶多元微分方程数值解法(其一)
  8. Qt学习-------常用控件
  9. 你不得不知道的通信行业基础介绍
  10. 《Head First 设计模式》(三):装饰者模式