U3D基础篇
经过了入门篇的打磨与体验,接下来进行基础篇的学习,其中包含动画,场景等设计。抓紧开始你的游戏之旅吧!
文章目录
- U3D基础篇
- 一些经历
-
-
-
- 1.调节游戏的运行参数
- 2.改进Update方法使其适应设备刷新率
- 3.角色移动
- 4.角色跳跃
- 5.CineMachine锁定摄像头
- 6.受击代码模板
- 7.生成怪物(面向对象的封装,继承和多态)
- 8.切换与重置场景
- 9.2D场景光效
- 10.使用PolyBrush绘制地图
- 11.实现鼠标移动事件
- 12.单例模式鼠标实现玩家移动
- 13.修改鼠标的指针模型
- 14.迷雾效果和Post Processing
- 15.动画树Blend Tree
- 16.Shader Graph遮挡剔除
- 17.从0开始创建一个怪物
- 18.追踪攻击敌人
- 19.怪物追踪与镜头视角修改
- 20.怪物的状态切换逻辑
- 21.怪物的随机巡逻点以及停留
- 22.使用ScriptableObject处理游戏数值
-
-
天空盒Skybox
天空盒:本质是一种Materials材质,Shader为Skybox/6 sided
。
设置方式:
- 1.准备天空盒材质
- 2.Window—>Rendering—>Lighting—>Environment—>Skybox Material,拖入材质即可。
理解:可以理解成3D世界中游戏的背景,本质是上下左右前后的六张贴图。
创建天空盒:
- 1.准备六个采用
无缝设计
的贴图,并将贴图的的WrapMode=Clamp
(无缝连接)完成设置后apply。 - 2.创建一个Material,设置
Shader=Skybox/6 Sided
,(锁定Inspector),将贴图对应拖入skybox即可。
宇宙与行星案例:
步骤:
- 1.设置天空盒
- 2.添加地球,倾角23.5°
- 3.地球自转
- 4.摄像机围绕地球旋转
- 5.背景音乐
动画初步
创建动画文件:
- 1.在Anim文件夹下创建一个
Animation
文件。 - 2.修改其类型为
Legacy
文件(旧版的,易上手),具体操作为右键检查器,切换为Debug
模式,勾选Legacy即可。 - 3.将其作为一个Animation组件挂载到游戏对象上即可。
调出Animation窗口,实际上该窗口与一些剪视频的软件布局极其相似,可以类比理解。
观察运动:
在观察物体运动时,可以采用以下两种方式:
- 1.Dopesheet简报模式:Pr显示模式,更倾向数值
- 2.Curves曲线模式:更倾向观察变换趋势与运动状态变化
F键:完全显示所有曲线,或者选中的关键帧。
练习:将物体改为匀速运动:
- 1.进入
Recording Mode
开始编辑 - 2.左侧,选中Rotation.x,只显示一条X曲线
- 3.编辑关键帧,分别选中第0帧和第60帧:
右键--->Both Tangent--->Linear
曲线的编辑:
对关键帧左右进行操作:
- Free:自由调整(贝塞尔曲线)
- Linear:直线
- Constant:固定值
子节点的动画:
案例:
- 1.导入一个直升机的模型
- 2.添加一个 Legacy 动画
- 3.为主翼 Top_Rotor和尾翼 Back_Roto r添加旋转效果。
动画事件Animation Event:
- 1.给物体添加一个脚本,Ball.cs
- 2.在脚本中添加一个回调函数
public void DropOnGround(float value)
{
Debug.Log("小球降落到最低点");
}
- 3.在动画编辑窗口,选中某一帧,右键选择
Add Animation Event
,右侧选择回调,设定参数的值即可。参数的类型可以是 float,int,string,GameObject等等。
动画状态机
动画状态机:一个基于状态机控制的动画系统。
状态机:一个事物或系统,有多种状态,控制它在各种状态间相互转换的机制。
案例:
一个人物模型存在以下几种状态:静止,走,跳,泅渡,攻击。
状态机的使用:
- 1.添加一个物体,机器人Robot
- 2.创建一个资源 AnimatorController
- 3.将Controller状态机挂载到物体Robot上
Animator窗口中,按F键可完全显示所有状态方块。
状态机的状态:
- Any State:
- Entry:状态机起点 ※
- Exit:
状态转换:
设置默认状态:右键某状态—>Set As Layer Default State
创建一个状态过度:右键某状态—>Make Transition
绑定动作:
- 1.准备好一个动画文件,选中某个状态
- 2.将动画文件拖入状态机某个状态的Motion下
- 3.选中Robot物体,在Animation窗口编辑舞蹈动画
-
- 勾选
Loop Time
表示循环播放
- 勾选
-
- 类型不要勾选
Legacy
- 类型不要勾选
状态参数与动画过渡:
如何判断物体应该过渡到某状态了呢?
- 1.在状态机左侧添加一个状态变量,类型可以是int,float,bool等
- 2.选中状态转移之间的线,取消勾选Has Exit Time(暂时不用,表示时间到了自动发生)
- 3.在Conditions中添加变量条件即可,当变量条件满足时,会进行状态的转移
Exit Time:
设置方法:
- 1.勾选Has Exit Time
- 2.设置退出时间
Exit Time
- 3.取消勾选
Fixed Duration
,如果勾选则单位按秒计算;否则,按圈计算。
状态机相关API:
//状态机相关运行,由状态变量来控制
//拿到Animator组件
Animator animator = GetComponent<Animator>();
//varName = value
animator.SetBool(varName,value);
animator.setInt(...);
...
状态机行为:
状态机行为:即立足于状态机上的回调函数
存在以下几种情况的回调:
- OnStateEnter():进入该状态时
- OnStateUpdate():进入该状态,每一次update()以后
- OnStateExit():退出该状态时
应用:在操控完对象某种状态时,将状态变量归未,防止重复进入某状态的bug。
更精细的控制:
问题提出:当机器球处在变身状态时,本应不能移动,但在该项目中仍可以移动,显然是一个问题。
解决方法:
//方法一:
//获取当前状态的API
anim.GetCurrentAnimatorStateInfo(0).IsName("状态名");
//方法二:
//自己添加一个额外的状态变量进行控制
地形系统Terrain
地形系统:山川河流,地表地貌
地表Terrain Layer:
- 新建一个地表Terrain Layer,相当于Material
- 指定贴图:Diffuse主帖图 + Normal 法线贴图
- 在Terrain编辑器中,选择第二个工具按钮绘制地形
- 下拉列表中选择
Paint Texture
- 选择
Edit Terrain Layers
,添加进创建的TerrainLayer即可。 - 绘制地表材质
花草:
- 选择
Paint Details
- 导入花草的贴图
一些地图渲染的细节:
- 1.此处的花草是2D贴图,并不是3D模型,用以降低GPU的负担
- 2.在离摄像机较近时,会自动扭转以正面面对摄像机
- 3.在离摄像机较远时,花草细节不被渲染
树木:
- 1.准备树的模型资源,添加树(选择 Paint Trees)
- 2.设置种植参数,批量种树即可。
- 3.给树添加碰撞检测
山峦:
- 在Paint Terrain中,设置为
Ralse or Lower Terrain
- 手动批量造山即可
盆地:
- 在Paint Terrain中,设置为
Set Height
- 手动批量造坑即可
按住shift键可以降低地势。
一些经历
1.调节游戏的运行参数
//设置游戏运行帧数
Application.targetFrameRate = ?;
//退出游戏
Application.Quit();
//暂停和复原游戏函数
void PauseGame(){
//调出暂停UI
PauseManu.SetActive(true);
//设置时停
Time.timeScale = 0f;
}
void ResumeGame(){
//调整暂停UI
PauseManu.SetActive(false);
//设置时停
Time.timeScale = 1f;
}
2.改进Update方法使其适应设备刷新率
void FixedUpdate(){
}
3.角色移动
//此处应拖入玩家类上的刚体组件
public RigidBody2D rigidBody;
//拖入玩家类上的动画组件
public Animator anim;
void Movement()
{
float horizontalMove = Input.GetAxis("Horizontal");
//Debug.Log(horizontalMove);[-1,1]
float faceDirection = Input.GetAxisRaw("Horizontal");
//Debug.Log(faceDirection);{-1,0,1}
//角色移动
if (horizontalMove != 0)
{
rigidbody.velocity = new Vector2(horizontalMove * speed, rigidbody.velocity.y) ;
anim.SetBool("isRun", true);
}
else
{
anim.SetBool("isRun", false);
}
//角色转向
if (faceDirection != 0)
{
transform.localScale = new Vector3(faceDirection, 1, 1);
}
}
4.角色跳跃
void Update()
{
Movement();
SwitchAnim();
}
void Movement()
{
//角色跳跃
if (Input.GetButtonDown("Jump"))
{
rigidbody.velocity = new Vector2(rigidbody.velocity.x, jumpForce);
anim.SetBool("isJump", true);
}
}
void SwitchAnim()
{
anim.SetBool("idle", false);
if (anim.GetBool("isJump") && rigidbody.velocity.y < 0)
{
anim.SetBool("isJump", false);
anim.SetBool("isFall", true);
}else if (coll.IsTouchingLayers(ground))
{
anim.SetBool("isFall", false);
anim.SetBool("idle", true);
}
}
5.CineMachine锁定摄像头
1.添加组件Cinemachine:
Windows—>PackageManager—>搜索安装即可
2.添加虚拟摄像头:
GameObject—>Cinemachine—>Visual Cemera
3.设置参量:
1)设置摄像头跟随follow
2)设置摄像头移动边界:添加Cinemachine Confiner 2D,并设置Bounding Shape 2D,一般需要给背景设置碰撞体检测,使其视像头绑定在规定边界。
6.受击代码模板
//消灭怪物
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Enemy")
{
if (anim.GetBool("isFall"))
{
Destroy(collision.gameObject);
rigidbody.velocity = new Vector2(rigidbody.velocity.x, jumpForce);
anim.SetBool("isJump", true);
}else if(transform.position.x < collision.gameObject.transform.position.x)
{
rigidbody.velocity = new Vector2(-10, rigidbody.velocity.y);
isHurt = true;
}
else if (transform.position.x > collision.gameObject.transform.position.x)
{
rigidbody.velocity = new Vector2(10, rigidbody.velocity.y);
isHurt = true;
}
}
}
7.生成怪物(面向对象的封装,继承和多态)
在未来在制作繁多种类的怪物时,可以抽取其共同特征来形成一个父类,后面直接通过父类生成具有各自特征的子类能大大降低代码的耦合度,从而时程序效率更高。
案例:怪物父类
public class Enemy : MonoBehaviour
{
//动画
protected Animator anim;
//音效
protected AudioSource deathAudio;
//虚拟的
protected virtual void Start()
{
anim = GetComponent<Animator>();
deathAudio = GetComponent<AudioSource>();
}
//通用方法——死亡
public void Death()
{
deathAudio.Play();
Destroy(gameObject);
}
}
在子类中调用:
public class Zombies : Enemy
{
protected override void Start()
{
//调用父类Enemy的Start方法
base.Start();
}
}
8.切换与重置场景
//加入场景相关库
using UnityEngine.SceneManagement;
//重置游戏逻辑
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
//切换下一关
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex+1);
9.2D场景光效
- 切换物体的材质为
Default-Diffuse
,也可自建材质为Diffuse。 - 添加Light组件,即可照亮以上材质光照效果
- 也可以给玩家本身添加点光源,使其范围呈现动画效果的扩散与缩小。
10.使用PolyBrush绘制地图
- 在
Package Manager
中引入Polybrush
插件,并导入Shader Examples(URP)
。 - 在
Tools
中开启 Polybrush的窗口,其中可以进行 地形绘制,地面平滑,颜色绘制,场景绘制等等。
11.实现鼠标移动事件
在脚本上属性拖拽为玩家结点以及 Nav Mesh Agent 的 destination。
[System.Serializable]
public class EventVector3 : UnityEvent<Vector3>
{
}
public class MouseManager : MonoBehaviour
{
//射线碰撞到的物体信息
RaycastHit hitInfo;
//鼠标点击事件
public EventVector3 OnMouseClicked;
void Update()
{
SetCursorTexture();
MouseControl();
}
//设置指针的贴图
void SetCursorTexture()
{
//从主摄像机的位置发射一道射线到鼠标所在位置
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
//将射线ray射到的物体返回给hitInfo变量
if (Physics.Raycast(ray,out hitInfo))
{
//切换鼠标贴图
}
}
void MouseControl()
{
//点击鼠标左键并且点击位置存在物体
if (Input.GetMouseButtonDown(0) && hitInfo.collider != null)
{
//点击到地面则操作挂载到人物上的事件
if (hitInfo.collider.gameObject.CompareTag("Ground"))
{
//判断事件是否为空,不为空则执行Invoke
OnMouseClicked?.Invoke(hitInfo.point);
}
}
}
}
12.单例模式鼠标实现玩家移动
在鼠标控制类上:MouseManager
using System;
//添加代理
public static MouseManager Instance;
//添加鼠标点击事件
public event Action<Vector3> OnMouseClicked;
void Awake()
{
if (Instance != null)
{
Destroy(this.gameObject);
}
Instance = this;
}
在控制玩家的类上:PlayerController
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : MonoBehaviour
{
private NavMeshAgent agent;
void Awake()
{
agent = GetComponent<NavMeshAgent>();
}
void Start()
{
//给鼠标添加一个事件函数
MouseManager.Instance.OnMouseClicked += MoveToTarget;
}
public void MoveToTarget(Vector3 target)
{
agent.destination = target;
}
}
13.修改鼠标的指针模型
//鼠标变换图标
public Texture2D point, doorway, attack, target, arrow;
//设置指针的贴图
void SetCursorTexture()
{
//从主摄像机的位置发射一道射线到鼠标所在位置
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
//将射线ray射到的物体返回给hitInfo变量
if (Physics.Raycast(ray,out hitInfo))
{
//切换鼠标贴图
switch (hitInfo.collider.gameObject.tag)
{
case "Ground":
Cursor.SetCursor(target, new Vector2(16, 16), CursorMode.Auto);
break;
}
}
}
14.迷雾效果和Post Processing
- 在Lighting界面勾选
fog
并在场景中开启fog - 调整可见度等参数即可
- 创建一个
volume
(Global or Box…),创建new一个Profile - 在场景中开启
open processing
,在Main Cemera
中Rendering
中启动post processing
- 设置Volume的参数,以下作为参考:
15.动画树Blend Tree
- 设置参数控制人物不同的动画
- 添加参数阈值以控制动画
在Update中实时调用切换动画:
public void SwitchAnimation()
{
//anim.SetFloat("Speed", agent.velocity.sqrMagnitude);
anim.SetFloat("Speed", agent.velocity.magnitude);
}
16.Shader Graph遮挡剔除
- 在Materials下建立一个 Shader-URP-Unlit Shader Graph,叫做 Occlusion Shader
- 在Occulsion下右键新建材质material
- 双击Occlusion Shader,打开以下面板:
-
修改URP通用渲染管线:
-
记得修改Player的图层,成功实现人物遮挡剔除效果
但此时树模型仍会遮挡到相机发出的射线。
解决办法:
方法一:
- 直接修改所有障碍物的图层为
Ignore Raycast
,即可忽略射线使玩家正常移动。
方法二:
- 取消tree的
Mesh Collider
组件,没有碰撞射线自然不会接触到障碍物了。
17.从0开始创建一个怪物
- 1.导入怪物的素材包
- 2.创建EnemyController脚本并通过脚本创建NavMesh Agent
- 3.设置Agent相关参数,比如碰撞体,速度等
- 4.通过枚举变量记录怪物的状态,并修改鼠标指针到攻击图标即可:
- 5.怪物脚本代码模板如下:
public enum EnemyStates {
GUARD,PATROL,CHASE,DEAD}
[RequireComponent(typeof(NavMeshAgent))]
public class EnemyController : MonoBehaviour
{
//agent的控制器
private NavMeshAgent agent;
//怪物的状态(区分追逐怪和静止怪)
public EnemyStates state;
void Awake()
{
agent = GetComponent<NavMeshAgent>();
}
void Update()
{
SwitchStates();
}
void SwitchStates()
{
switch (state)
{
case EnemyStates.GUARD:
break;
case EnemyStates.PATROL:
break;
case EnemyStates.CHASE:
break;
case EnemyStates.DEAD:
break;
}
}
}
18.追踪攻击敌人
在MouseManager中:
//创建一个新的点击事件
public event Action<GameObject> OnEnemyClicked;
//触发事件
void MouseControl()
{
//点击鼠标左键并且点击位置存在物体
if (Input.GetMouseButtonDown(0) && hitInfo.collider != null)
{
//点击到地面则操作挂载到人物上的事件
if (hitInfo.collider.gameObject.CompareTag("Ground"))
{
//判断事件是否为空,不为空则执行Invoke
OnMouseClicked?.Invoke(hitInfo.point);
}
if (hitInfo.collider.gameObject.CompareTag("Enemy"))
{
//判断事件是否为空,不为空则执行Invoke
OnEnemyClicked?.Invoke(hitInfo.collider.gameObject);
}
}
}
在PlayerController中:
//开始直接将点击事件注册到Start方法
//攻击目标
private GameObject attackTarget;
//攻击冷却时间
private float lastAttackTime;
void Start()
{
MouseManager.Instance.OnMouseClicked += MoveToTarget;
MouseManager.Instance.OnEnemyClicked += MoveToAttackTarget;
}
void Update()
{
SwitchAnimation();
//冷却衰减
lastAttackTime -= Time.deltaTime;
}
private void EventAttack(GameObject target)
{
//判断攻击目标是否为空
if (target != null)
{
attackTarget = target;
//调用协程方法
StartCoroutine(MoveToAttackTarget());
}
}
//写一个携程进行攻击检测
IEnumerator MoveToAttackTarget()
{
agent.isStopped = false;
transform.LookAt(attackTarget.transform);
//如果两者距离大于攻击距离,则需要先跑动至攻击距离以内
while (Vector3.Distance(attackTarget.transform.position,transform.position) > 1)
{
agent.destination = attackTarget.transform.position;
yield return null;
}
agent.isStopped = true;
//Attack
if (lastAttackTime < 0)
{
anim.SetTrigger("Attack");
//重置冷却时间
lastAttackTime = 0.5f;
}
}
但此时攻击完无法移动,需要在移动行为函数中还原isStopped变量:
还需在攻击动画播放时,能打断攻击的行为:
解决以上两点问题,可修改移动行为如下:
public void MoveToTarget(Vector3 target)
{
StopAllCoroutines();
agent.isStopped = false;
agent.destination = target;
}
19.怪物追踪与镜头视角修改
- FreeLook摄像机的使用
怪物发现玩家代码逻辑:
[Header("Basic Settings")]
//发现范围半径
public float sightRadius;
void SwitchStates()
{
//如果发现player 切换到CHASE
if (FoundPlayer())
{
state = EnemyStates.CHASE;
//Debug.Log("发现Player");
}
switch (state)
{
case EnemyStates.GUARD:
break;
case EnemyStates.PATROL:
break;
case EnemyStates.CHASE:
break;
case EnemyStates.DEAD:
break;
}
}
bool FoundPlayer()
{
var colliders = Physics.OverlapSphere(transform.position, sightRadius);
foreach (var target in colliders)
{
if (target.CompareTag("Player"))
{
return true;
}
}
return false;
}
20.怪物的状态切换逻辑
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public enum EnemyStates {
GUARD,PATROL,CHASE,DEAD}
[RequireComponent(typeof(NavMeshAgent))]
public class EnemyController : MonoBehaviour
{
//agent的控制器
private NavMeshAgent agent;
//怪物的状态
private EnemyStates state;
//动画控制器
private Animator anim;
[Header("Basic Settings")]
public float sightRadius;
//怪物是否为巡逻或静止
public bool isGuard;
private float speed;
//获取玩家
private GameObject attackTarget;
//bool配合动画
bool isWalk, isChase, isFollow;
void Awake()
{
agent = GetComponent<NavMeshAgent>();
speed = agent.speed;
anim = GetComponent<Animator>();
}
void Update()
{
SwitchStates();
SwitchAnimation();
}
void SwitchAnimation()
{
anim.SetBool("Walk", isWalk);
anim.SetBool("Chase", isChase);
anim.SetBool("Follow", isFollow);
}
void SwitchStates()
{
//如果发现player 切换到CHASE
if (FoundPlayer())
{
state = EnemyStates.CHASE;
//Debug.Log("发现Player");
}
switch (state)
{
case EnemyStates.GUARD:
break;
case EnemyStates.PATROL:
break;
case EnemyStates.CHASE:
//TODO:追Player,如果超出范围则回到上一个状态
//TODO:执行动画,在攻击范围内发起攻击
isWalk = false;
isChase = true;
agent.speed = speed;
if (!FoundPlayer())
{
isFollow = false;
agent.destination = transform.position;
}
else
{
isFollow = true;
agent.destination = attackTarget.transform.position;
}
break;
case EnemyStates.DEAD:
break;
}
}
bool FoundPlayer()
{
var colliders = Physics.OverlapSphere(transform.position, sightRadius);
foreach (var target in colliders)
{
if (target.CompareTag("Player"))
{
attackTarget = target.gameObject;
return true;
}
}
attackTarget = null;
return false;
}
}
21.怪物的随机巡逻点以及停留
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public enum EnemyStates {
GUARD,PATROL,CHASE,DEAD}
[RequireComponent(typeof(NavMeshAgent))]
public class EnemyController : MonoBehaviour
{
//agent的控制器
private NavMeshAgent agent;
//怪物的状态
private EnemyStates state;
//动画控制器
private Animator anim;
[Header("Basic Settings")]
public float sightRadius;
//怪物是否为巡逻或静止
public bool isGuard;
private float speed;
//获取玩家
private GameObject attackTarget;
//巡逻停留时间
public float lookAtTime;
private float remainLookAtTime;
//巡逻范围
[Header("Patrol State")]
public float patrolRange;
private Vector3 wayPoint;
private Vector3 guardPos;
//bool配合动画
bool isWalk, isChase, isFollow;
void Awake()
{
agent = GetComponent<NavMeshAgent>();
speed = agent.speed;
anim = GetComponent<Animator>();
guardPos = transform.position;
remainLookAtTime = lookAtTime;
}
void Start()
{
if (isGuard)
{
state = EnemyStates.GUARD;
}
else
{
state = EnemyStates.PATROL;
GetNewWayPoint();
}
}
void Update()
{
SwitchStates();
SwitchAnimation();
}
void SwitchAnimation()
{
anim.SetBool("Walk", isWalk);
anim.SetBool("Chase", isChase);
anim.SetBool("Follow", isFollow);
}
void SwitchStates()
{
//如果发现player 切换到CHASE
if (FoundPlayer())
{
state = EnemyStates.CHASE;
//Debug.Log("发现Player");
}
switch (state)
{
case EnemyStates.GUARD:
break;
case EnemyStates.PATROL:
isChase = false;
agent.speed = speed * 0.5f;
if(Vector3.Distance(wayPoint,transform.position) <= agent.stoppingDistance)
{
isWalk = false;
if (remainLookAtTime > 0)
{
remainLookAtTime -= Time.deltaTime;
}
else
{
GetNewWayPoint();
}
}
else
{
isWalk = true;
agent.destination = wayPoint;
}
break;
case EnemyStates.CHASE:
//TODO:追Player,如果超出范围则回到上一个状态
//TODO:执行动画,在攻击范围内发起攻击
isWalk = false;
isChase = true;
agent.speed = speed;
if (!FoundPlayer())
{
isFollow = false;
if (remainLookAtTime > 0)
{
agent.destination = transform.position;
remainLookAtTime -= Time.deltaTime;
}
else if(isGuard)
{
state = EnemyStates.GUARD;
}
else
{
state = EnemyStates.PATROL;
}
}
else
{
isFollow = true;
agent.destination = attackTarget.transform.position;
}
break;
case EnemyStates.DEAD:
break;
}
}
bool FoundPlayer()
{
var colliders = Physics.OverlapSphere(transform.position, sightRadius);
foreach (var target in colliders)
{
if (target.CompareTag("Player"))
{
attackTarget = target.gameObject;
return true;
}
}
attackTarget = null;
return false;
}
void GetNewWayPoint()
{
remainLookAtTime = lookAtTime;
float randomX = Random.Range(-patrolRange, patrolRange);
float randomZ = Random.Range(-patrolRange, patrolRange);
Vector3 randomPoint = new Vector3(guardPos.x + randomX, guardPos.y ,guardPos.z + randomZ);
//碰撞体检测,防止怪物卡住
NavMeshHit hit;
//如果随机点不是Walkable,则会重新获取随机点
wayPoint = NavMesh.SamplePosition(randomPoint, out hit, patrolRange, 1) ? hit.position : transform.position;
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, sightRadius);
}
}
22.使用ScriptableObject处理游戏数值
- 创建数值脚本
AttackData_SO
文件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName ="New Attack",menuName ="Attack/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;
}
- 在Game Data下创建新的文件夹Attack Data,在其中创建数据集文件
Attack-->Attack Data
,并命名为Player BaseAttackData
。 - 在模板文件中设置数值:
- 写数据读取文件CharacterStats_SO:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CharacterStats : MonoBehaviour
{
public CharacterData_SO characterData;
public AttackData_SO attackData;
#region Read from Data_SO
public 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 currentHealth
{
get
{
return characterData != null ? characterData.currentDefence : 0;
}
set
{
characterData.currentDefence = value;
}
}
}
#endregion