经过了入门篇的打磨与体验,接下来进行基础篇的学习,其中包含动画,场景等设计。抓紧开始你的游戏之旅吧!
天空盒:本质是一种Materials材质,Shader为Skybox/6 sided
。
设置方式:
理解:可以理解成3D世界中游戏的背景,本质是上下左右前后的六张贴图。
创建天空盒:
无缝设计
的贴图,并将贴图的的WrapMode=Clamp
(无缝连接)完成设置后apply。Shader=Skybox/6 Sided
,(锁定Inspector),将贴图对应拖入skybox即可。宇宙与行星案例:
步骤:
创建动画文件:
Animation
文件。Legacy
文件(旧版的,易上手),具体操作为右键检查器,切换为Debug
模式,勾选Legacy即可。调出Animation窗口,实际上该窗口与一些剪视频的软件布局极其相似,可以类比理解。
观察运动:
在观察物体运动时,可以采用以下两种方式:
F键:完全显示所有曲线,或者选中的关键帧。
练习:将物体改为匀速运动:
Recording Mode
开始编辑右键--->Both Tangent--->Linear
曲线的编辑:
对关键帧左右进行操作:
子节点的动画:
案例:
动画事件Animation Event:
public void DropOnGround(float value)
{
Debug.Log("小球降落到最低点");
}
Add Animation Event
,右侧选择回调,设定参数的值即可。参数的类型可以是 float,int,string,GameObject等等。动画状态机:一个基于状态机控制的动画系统。
状态机:一个事物或系统,有多种状态,控制它在各种状态间相互转换的机制。
案例:
一个人物模型存在以下几种状态:静止,走,跳,泅渡,攻击。
状态机的使用:
Animator窗口中,按F键可完全显示所有状态方块。
状态机的状态:
状态转换:
设置默认状态:右键某状态—>Set As Layer Default State
创建一个状态过度:右键某状态—>Make Transition
绑定动作:
Loop Time
表示循环播放Legacy
状态参数与动画过渡:
如何判断物体应该过渡到某状态了呢?
Exit Time:
设置方法:
Exit Time
Fixed Duration
,如果勾选则单位按秒计算;否则,按圈计算。状态机相关API:
//状态机相关运行,由状态变量来控制
//拿到Animator组件
Animator animator = GetComponent<Animator>();
//varName = value
animator.SetBool(varName,value);
animator.setInt(...);
...
状态机行为:
状态机行为:即立足于状态机上的回调函数
存在以下几种情况的回调:
应用:在操控完对象某种状态时,将状态变量归未,防止重复进入某状态的bug。
更精细的控制:
问题提出:当机器球处在变身状态时,本应不能移动,但在该项目中仍可以移动,显然是一个问题。
解决方法:
//方法一:
//获取当前状态的API
anim.GetCurrentAnimatorStateInfo(0).IsName("状态名");
//方法二:
//自己添加一个额外的状态变量进行控制
地形系统:山川河流,地表地貌
地表Terrain Layer:
Paint Texture
Edit Terrain Layers
,添加进创建的TerrainLayer即可。花草:
Paint Details
一些地图渲染的细节:
树木:
山峦:
Ralse or Lower Terrain
盆地:
Set Height
按住shift键可以降低地势。
//设置游戏运行帧数
Application.targetFrameRate = ?;
//退出游戏
Application.Quit();
//暂停和复原游戏函数
void PauseGame(){
//调出暂停UI
PauseManu.SetActive(true);
//设置时停
Time.timeScale = 0f;
}
void ResumeGame(){
//调整暂停UI
PauseManu.SetActive(false);
//设置时停
Time.timeScale = 1f;
}
void FixedUpdate(){}
//此处应拖入玩家类上的刚体组件
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);
}
}
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);
}
}
1.添加组件Cinemachine:
Windows—>PackageManager—>搜索安装即可
2.添加虚拟摄像头:
GameObject—>Cinemachine—>Visual Cemera
3.设置参量:
1)设置摄像头跟随follow
2)设置摄像头移动边界:添加Cinemachine Confiner 2D,并设置Bounding Shape 2D,一般需要给背景设置碰撞体检测,使其视像头绑定在规定边界。
//消灭怪物
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;
}
}
}
在未来在制作繁多种类的怪物时,可以抽取其共同特征来形成一个父类,后面直接通过父类生成具有各自特征的子类能大大降低代码的耦合度,从而时程序效率更高。
案例:怪物父类
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();
}
}
//加入场景相关库
using UnityEngine.SceneManagement;
//重置游戏逻辑
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
//切换下一关
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex+1);
Default-Diffuse
,也可自建材质为Diffuse。 Package Manager
中引入 Polybrush
插件,并导入 Shader Examples(URP)
。Tools
中开启 Polybrush的窗口,其中可以进行 地形绘制,地面平滑,颜色绘制,场景绘制等等。在脚本上属性拖拽为玩家结点以及 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);
}
}
}
}
在鼠标控制类上: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;
}
}
//鼠标变换图标
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;
}
}
}
fog
并在场景中开启fogvolume
(Global or Box…),创建new一个Profileopen processing
,在Main Cemera
中Rendering
中启动 post processing
public void SwitchAnimation()
{
//anim.SetFloat("Speed", agent.velocity.sqrMagnitude);
anim.SetFloat("Speed", agent.velocity.magnitude);
}
但此时树模型仍会遮挡到相机发出的射线。
解决办法:
方法一:
Ignore Raycast
,即可忽略射线使玩家正常移动。方法二:
Mesh Collider
组件,没有碰撞射线自然不会接触到障碍物了。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;
}
}
}
在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;
}
怪物发现玩家代码逻辑:
[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;
}
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;
}
}
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);
}
}
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;
}
Attack-->Attack Data
,并命名为Player BaseAttackData
。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