最近入手了一个RPG游戏,用来学习C#游戏编程与继承游戏框架的搭建,游戏是场景以LowPoly的底面模型进行搭建,玩家扮演士兵与怪兽进行战斗以提升自身等级与战斗力,游戏中也同时包含的传送门的功能,玩家可以在不同的场景之间进行穿梭。这个游戏主要是来进行游戏开发的学习,所以可玩性啥的也就不重要啦。
第一步想要开发一款游戏首先要对游戏有一个明确的定位与构思!
下面我们来明确一下自己的对这款游戏的定位与构思。游戏很简单,我们就是操作DogKing对敌人发起攻击,其中我们定义操作的方式为鼠标点击地面可以设置敌人的前进方向;点击敌人可以设置的人的攻击目标。敌人则是由不同的怪兽组成,不同的怪兽有着不同的行为方式与攻击数值。小怪兽我们可以根据需要设置为站桩或者巡逻模式,攻击力较低容易击杀可以为玩家提供经验以提升玩家的攻击力为后来与BOSS进行战斗做了铺垫。至此整个游戏的游玩逻辑以及已经明确,下一步就是从不同的方面为游戏添加细节,从而对游戏进行优化,丰富游戏内容。
游戏框架1----------->构建游戏场景
游戏采用低面多边形进行场景的构建,所以在这里我是用了PolyBrush、ProBulider,ProGrids这些插件就行游戏场景的搭建。当我们把基本的游戏场景搭建完毕后下一步就是烘焙地图导航。因为我们使用NavMeshAgent组件控制玩家与敌人的位移。选中我们的地形模型在监视器窗口的Navigation进行参数设置。
从上至下依次为:玩家的半径、玩家的高度、玩家最大爬坡坡度、步伐的高度、下跌高度、跳跃距离。
设置完毕后我们将其点击Bake。
Tips:我们要将需要进行烘焙导航的物体设置为Navigation Static。同时对于地面上的树木我们要在Object界面中将其设置为 Navigation Area Not Walkable
游戏框架2----------->MouseManger
为了在全局的角度对鼠标的点击事件进行处理,我们单独创建MouseManger脚本并把它进行单例化方便我们在全局进行调用
public class Mouse_Manger : Singleton<Mouse_Manger>
{
//使用了单例模式
public Texture2D Target, Normal, Attack, Exit;
//多种鼠标点击事件!!!
public event Action<Vector3> OnMouseclicked;
public event Action<GameObject> OnEnemyclicked;
RaycastHit hit;
protected override void Awake()
{
base.Awake();
DontDestroyOnLoad(this);
Debug.Log("Mouse"+Time.frameCount);
}
private void Update()
{
SetCueseTexture();
Mousecontroller();
}
void SetCueseTexture()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
switch (hit.collider.gameObject.tag)
{
case "Ground":
Cursor.SetCursor(Target, new Vector3(16, 16), CursorMode.Auto);
break;
case "Enemy":
Cursor.SetCursor(Attack, new Vector3(16, 16), CursorMode.Auto);
break;
case "AttackAble":
Cursor.SetCursor(Attack, new Vector3(16, 16), CursorMode.Auto);
break;
case "Portal":
Cursor.SetCursor(Exit, new Vector3(16, 16), CursorMode.Auto);
break;
}
}
}
void Mousecontroller()
{
if(Input.GetMouseButtonDown(0)&&hit.collider!=null)
{
if (hit.collider.CompareTag("Ground"))
{
OnMouseclicked?.Invoke(hit.point);
}
if (hit.collider.CompareTag("Portal"))
{
OnMouseclicked?.Invoke(hit.point);
}
if (hit.collider.CompareTag("Enemy")|| hit.collider.CompareTag("AttackAble"))
{
OnEnemyclicked?.Invoke(hit.collider.gameObject);
}
}
}
}
首先鼠标要对你点击的物体进行判断,通常我们用的是基于摄像机的射线检测。
我们先要生成一个射线,这个射线从摄像机发出指向我们的鼠标
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{不同的鼠标点击对象我们会为鼠标设置不同的鼠标图标,这里我们为不同的物体以Tag进行区分
switch (hit.collider.gameObject.tag)
{
case "Ground":设置一下鼠标偏移,鼠标图标的中心点为点击的作用点
Cursor.SetCursor(Target, new Vector3(16, 16), CursorMode.Auto);
break;
case "Enemy":
Cursor.SetCursor(Attack, new Vector3(16, 16), CursorMode.Auto);
break;
case "AttackAble":
Cursor.SetCursor(Attack, new Vector3(16, 16), CursorMode.Auto);
break;
case "Portal":
Cursor.SetCursor(Exit, new Vector3(16, 16), CursorMode.Auto);
break;}
}
设置完鼠标判断点击对象的方法后,下一步我们为不同的点击结果添加处理方法
public event Action<Vector3> OnMouseclicked;
public event Action<GameObject> OnEnemyclicked;
首先我们声明了两种Action<>事件,一种是点击地面的位移事件,一种是点击敌人攻击事件。Tips:通常我们在全局脚本中声明事件,在对象的脚本中进行订阅,为事件进行赋值。同时我们需要为该事件的订阅进行传参,将我们鼠标点击的对象传入到我们订阅中。
void Mousecontroller()
{
if(Input.GetMouseButtonDown(0)&&hit.collider!=null)
{
if (hit.collider.CompareTag("Ground"))
{判断鼠标点击事件是否被订阅,如果订阅我们就执行该事件的订阅,并将我们的鼠 标点击的目标的参数传入到订阅中去
OnMouseclicked?.Invoke(hit.point);
}
if (hit.collider.CompareTag("Portal"))
{
OnMouseclicked?.Invoke(hit.point);
}
if (hit.collider.CompareTag("Enemy")|| hit.collider.CompareTag("AttackAble"))
{
OnEnemyclicked?.Invoke(hit.collider.gameObject);
}
}
}
创建完了MouseManger下一步我们就要为玩家的机制做准备了
重点:
人物框架--Controller一般承载的人物最为核心的功能,例如移动、攻击、状态切换。我们把这些核心都放在Controller中,其余的杂碎功能我们则是在其他的脚本中书写,人物的Controller只负责调用和传参,这样可以更为方便的对人物的功能就行修改和创建。
游戏框架3----------->PlayerController
人物具有基本的属性,例如最大生命值,当前的生命值,防御值,攻击力等等,为了对这些数据好的管理调用和保存,我们使用ScriptsObject。
ScriptsObject就是一种用来储存数据的配置文件,他在Unity以文件的形式存在。具有以下的优点:
- 把数据真正存储在了资源文件中,可以像其他资源那样管理它,例如退出运行也一样会保持修改
- 可以在项目之间很好的复用,不用再制作Prefab那样导入导出
- 在概念上有很好的fit,强迫症患者的福音
首先我们来创建ScriptsObject
using System.Collections;
using System.Collections.Generic;
using UnityEngine;在Create中进行创建,我们可以通过UnityProject窗口Create菜单快速创建
filename默认文件名,menuname该配置文件的创建位置
[CreateAssetMenu(fileName ="New Date",menuName ="Character State/Date")]任何ScriptsObject文件都要继承 ScriptableObject
public class CharacterDate_SO : ScriptableObject
{
[Header("State Info")]
public int Maxhp;
public int Defence;
public int Currenthealth;
public int Level;
public int Experience;
}
我们将人物的生命数值相关的储存与攻击数值的相关储存分别创建两类的ScriptsObject的配置文件,随后我们为每个不同的角色分别创建不同的生命与攻击的相关配置文件并赋值。我们还需单独创建CharacterState脚本对人物的状态进行管理。
Tips:ScriptsObject是一个全局性文件,当我们对其更改时将会实时记录到文件中来,所以我们一般会动态创建一个副本的配置文件,对副本中的数据进行更改。
public CharacterDate_SO TempCharacterdate;
//生成配置文件的副本
public CharacterDate_SO CharacterDate;
public AttackDate_SO TempAttackDate;
//生成配置文件的副本
public AttackDate_SO AttackDate;private void Awake()
{
//创建人物的数据副本
if (TempCharacterdate != null)
{
CharacterDate = Instantiate(TempCharacterdate);
}
if(TempAttackDate!=null)
{
AttackDate = Instantiate(TempAttackDate);
}
}
人物的Characterstate已经对其进行管理,随后我们就要对PlayerController进行创建,对人物的行为进行规范。我们创建PlayerController脚本并将其挂载到Player身上。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : MonoBehaviour
{
private NavMeshAgent Agent;
private Animator Anim;
private GameObject attackObject;
private float lastattacktime=0;
private GameObject AttackTarget;
public float ChaseSpeed;
public float WalkSpeed;
private CharacterState characterState;
bool IsDead;
private void Awake()
{
Anim = this.GetComponent<Animator>();
Agent = this.GetComponent<NavMeshAgent>();
characterState = this.GetComponent<CharacterState>();
}
private void OnEnable()
{
Mouse_Manger.Instance.OnMouseclicked += MoveToTarget;
Mouse_Manger.Instance.OnEnemyclicked += EventAttack;
GameManager.Instance.Rigisterplayer(characterState);
}
private void Start()
{
}
private void OnDisable()
{
Mouse_Manger.Instance.OnMouseclicked -= MoveToTarget;
Mouse_Manger.Instance.OnEnemyclicked -= EventAttack;
}
private void Update()
{
ChangeState();
lastattacktime -=Time.deltaTime;
FaceToEnemy();
IfEnemyIsDead();
if(characterState.Currenthealth==0)
{
GameManager.Instance.NotifyObservers();
Anim.SetBool("Dead",true);
IsDead = true;
}
}
//判断敌人是否已经死亡
void IfEnemyIsDead()
{
if (attackObject != null)
{
if (attackObject.GetComponent<Collider>().enabled == false)
{
Anim.SetBool("Attack", false);
}
}
}
void FaceToEnemy()
{
if (attackObject != null)
{
if (Vector3.Distance(transform.position, attackObject.transform.position) < 3)
{
Quaternion forwardto = Quaternion.LookRotation(new Vector3(attackObject.transform.position.x, transform.position.y, attackObject.transform.position.z) - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, forwardto, 0.08f);
}
}
}
void ChangeState()
{
Anim.SetFloat("Speed",Agent.velocity.magnitude/3.5f);
}
void MoveToTarget(Vector3 Target)
{
if (IsDead) return;
Agent.stoppingDistance = 0.3f;
Agent.destination = Target;
Agent.isStopped = false;
Anim.SetBool("Attack",false);
}
private void EventAttack(GameObject obj)
{
if (IsDead) return;
Agent.stoppingDistance =0.7f;
Agent.destination = obj.transform.position;
attackObject = obj;
StartCoroutine(MoveToAttack());
}
IEnumerator MoveToAttack()
{
//由于敌人的体积不同,所以导航的停止距离也是各不相同
if (attackObject.GetComponent<NavMeshAgent>())
{
Agent.stoppingDistance = attackObject.GetComponent<NavMeshAgent>().radius + Agent.radius+0.5f;
while (Vector3.Distance(transform.position, attackObject.transform.position) > Agent.stoppingDistance)
{
yield return null;
}
}
if(attackObject.GetComponent<Rock>())
{
Agent.stoppingDistance = 1.5f;
while (Vector3.Distance(transform.position, attackObject.transform.position) >2)
{
yield return null;
Debug.Log("在路上");
}
Debug.Log("准备攻击");
}
//创建一个旋转到敌人的四元素
Agent.isStopped = true;
if (lastattacktime < 0)
{
characterState.Iscricital = UnityEngine.Random.value < characterState.AttackDate.CriticalChance;
Anim.SetBool("Attack", true);
lastattacktime = characterState.AttackDate.CD;
Debug.Log("玩家攻击");
}
else
Anim.SetBool("Attack", false);
}
void Hit()
{
if (attackObject != null)
{
if (attackObject.GetComponent<Rock>())
{
attackObject.GetComponent<Rigidbody>().AddForce(transform.forward*40,ForceMode.Impulse);
attackObject.GetComponent<Rock>().state = Rock.RockState.AttackEnemy;
Debug.Log("反击石头");
}
else
{
var enemystate = attackObject.GetComponent<CharacterState>();
enemystate.TakeDamag(characterState);
}
}
}
}
首先我们要对鼠标点击事件进行订阅,让我们的人物可以进行移动或者攻击。
private void OnEnable()
{
Mouse_Manger.Instance.OnMouseclicked += MoveToTarget;
Mouse_Manger.Instance.OnEnemyclicked += EventAttack;
GameManager.Instance.Rigisterplayer(characterState);
}
我们在Enable中获取到MouseManger的事件并对其进行订阅。 首先是移动到目标点
void MoveToTarget(Vector3 Target)
{判断敌人是否死亡,死亡后禁止移动
if (IsDead) return;对于地面移动来说,我们设置其停止距离相对较小提高移动的精度
Agent.stoppingDistance = 0.3f;设置鼠标点击的目标点为导航的目标点
Agent.destination = Target;
Agent.isStopped = false;敌人移动时禁止攻击
Anim.SetBool("Attack",false);
}
然后就是人物的攻击
private void EventAttack(GameObject obj)
{ 攻击时首先要移动到敌人的附近当敌人进入到我们的攻击距离时就可以对其攻击
if (IsDead) return;
Agent.stoppingDistance =0.7f;
Agent.destination = obj.transform.position;
attackObject = obj;
StartCoroutine(MoveToAttack());
}Tips:由于鼠标点击是一帧的事情,而玩家移动到敌人并发起攻击是一个持续的过程所以这时候我们要采用协程的办法来实现。
IEnumerator MoveToAttack()
{
//由于敌人的体积不同,所以导航的停止距离也是各不相同
if (attackObject.GetComponent<NavMeshAgent>())
{
Agent.stoppingDistance = attackObject.GetComponent<NavMeshAgent>().radius + Agent.radius+0.5f;通过While循环来判断是否进入攻击距离,如果没到就让这个判断延迟一帧从而叨叨持续判断的效果
while (Vector3.Distance(transform.position, attackObject.transform.position) > Agent.stoppingDistance)
{只要在While循环内,这个方法就会进行延迟一帧,从而达到在内部创建Update的目的
yield return null;
}
}
if(attackObject.GetComponent<Rock>())
{
Agent.stoppingDistance = 1.5f;
while (Vector3.Distance(transform.position, attackObject.transform.position) >2)
{
yield return null;
Debug.Log("在路上");
}
Debug.Log("准备攻击");
}当敌人进入攻击距离Agent停止开始攻击
Agent.isStopped = true;进行攻击间隔的判断
if (lastattacktime < 0)
{判断是否发生暴击
characterState.Iscricital = UnityEngine.Random.value < characterState.AttackDate.CriticalChance;
Anim.SetBool("Attack", true);
lastattacktime = characterState.AttackDate.CD;
Debug.Log("玩家攻击");
}
else
Anim.SetBool("Attack", false);
}
至此我们实现了玩家移动和攻击的效果 下一步我们要进行敌人的脚本书写
游戏框架4----------->EnemyController
首先敌人的状态相对于玩家来说要更复杂,我们首先要对敌人的状态精心构思
根据上述的敌人状态转换图我们将敌人的状态以枚举的形式进行定义
public enum EnenmtState
{
Guard,巡逻
Patrol,站桩
Chase,追击
Dead死亡
}
状态还可以更加细分,这里没有苛刻的定义准则。为了简单理解我们将攻击状态融入到追击里面。
我们创建一个状态选择的方法
void SwitchState()
{这里首先判断一下敌人是否死亡
if (EnemyState.Currenthealth == 0)
{
State = EnenmtState.Dead;
}
switch (State)
{
//巡逻
case EnenmtState.Guard:if (FindPlayer()&& EnemyState.Currenthealth != 0)
{
//重置等待时间,为丢失玩家后发呆做准备
remaintime = 0;
animCatch = true;
State = EnenmtState.Chase;
break;
}
Agent.destination = Waypoint;
if (Vector3.Distance(new Vector3(Waypoint.x,0,Waypoint.z), new Vector3(transform.position.x,0, transform.position.z)) <= Agent.stoppingDistance)
{
remaintime += Time.deltaTime;
if (remaintime > Staytime)
{
Guard();
remaintime =0;
}
}
break;
case EnenmtState.Patrol:
Agent.destination = restposition;
Anim.SetBool("Back",true);
if (Agent.remainingDistance <Agent.stoppingDistance)
{
LookForward();
Anim.SetBool("Back", false);
}
break;
case EnenmtState.Chase:
Agent.destination = AttactTarget.position;
//没追到
if (!FindPlayer())
{
animCatch = false;
remaintime += Time.deltaTime;
Agent.destination = transform.position;
Debug.Log("丢失玩家");
if (remaintime > Staytime)
{
if (Ifguard)
{
State = EnenmtState.Guard;
}
else
{
State = EnenmtState.Patrol;
}
remaintime = 0;
}
}
//追到进行攻击
AttackEnemy();break;
case EnenmtState.Dead:
collider.enabled = false;
Anim.SetBool("Dead",true);
Destroy(this.gameObject, 2f);
break;}
}
我们在当前状态中添加对可能进行状态转换的其他状态的判断,如果在当前状态中符合转换条件我们就设置状态进行变化。
Tips:人物的巡逻机制我们是利用随机巡航点来实现的,这里有一些问题要注意。既然是随机巡航点我们就要避免随机巡航点声称在我们导航网格禁止到达的点。所以我们要对这些点进行处理加工。
void Guard()
{
NavMeshHit hit;
初始时声明一个不在网格的目标点
Waypoint = new Vector3(100,-100,1000);
NavMesh.SamplePosition(Waypoint, out hit, 1f, 1)可以判断导航点是否在导航网格上并 随机返回一个以目标点圆心固定半径的一个随机导航点
while (!NavMesh.SamplePosition(Waypoint, out hit, 1f, 1))
{ 进行While循环直到WayPoint在导航网格上
Waypoint = new Vector3(restposition.x + Random.Range(-Range, Range), restposition.y, restposition.z + Random.Range(-Range, Range));
if (NavMesh.SamplePosition(Waypoint, out hit, 1f, 1))
{
Waypoint = hit.position;
return;
}
}
}
至此,我们对敌人与玩家的基本运行机制已经制作完毕,实现了玩家移动攻击和敌人巡逻追击攻击等功能。下一步我们就是要为玩家和敌人完善生命系统的相关功能。
游戏框架5----------->生命管理系统
前面我们提到了CharacterState来储存人物的状态,例如生命值、攻击力啥的。所以我们的生命管理系统也要依托与CharacterState来进行实现。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CharacterState : MonoBehaviour
{
public CharacterDate_SO TempCharacterdate;
//生成配置文件的副本
public CharacterDate_SO CharacterDate;
public AttackDate_SO TempAttackDate;
//生成配置文件的副本
public AttackDate_SO AttackDate;
public Animator Anim;
//标记的被暴击!!!!!!!!
public bool Iscricital;
public Action<int, int> UPdateHealthUI;
public int CurrentDefence
{
get
{
if (CharacterDate != null)
{
return CharacterDate.Defence;
}
else
return 0;
}
set
{
CurrentDefence= value;
}
}
public int Currenthealth
{
get
{
return CharacterDate.Currenthealth;
}
set
{
CharacterDate.Currenthealth = value;
}
}
public int CurrentLevel
{
get
{
return CharacterDate.Level;
}
set
{
CharacterDate.Level= value;
}
}
public int Exeprience
{
get
{
return CharacterDate.Experience;
}
set
{
CharacterDate.Experience = value;
}
}
private void Awake()
{
//创建人物的数据副本
if (TempCharacterdate != null)
{
CharacterDate = Instantiate(TempCharacterdate);
}
if(TempAttackDate!=null)
{
AttackDate = Instantiate(TempAttackDate);
}
}
private void Start()
{
Currenthealth = CharacterDate.Maxhp;
Anim = this.GetComponent<Animator>();
}
private void Update()
{
}
#region
//谁被攻击,谁调用接受伤害的方法,进行减血
public void TakeDamag(CharacterState attack)
{
//接受传入的攻击者的伤害
int damage = Math.Max(attack.CurrentDamage() - CharacterDate.Defence,1);
//如果被暴击
//if (Iscricital)
//{
// Anim.SetTrigger("GetHit");
//}
Currenthealth -= damage;
Currenthealth = Math.Max(Currenthealth,0);
if (Currenthealth <= 0)
{
attack.AddPoint(30);
}
UPdateHealthUI?.Invoke(Currenthealth, CharacterDate.Maxhp);
if (this.GetComponent<HealthSlider>())
{
this.GetComponent<HealthSlider>().SeeTime = 3;
}
}
public void TakeDamag(int damage)
{
Currenthealth -= damage-CharacterDate.Defence;
Currenthealth = Math.Max(Currenthealth, 0);
UPdateHealthUI?.Invoke(Currenthealth, CharacterDate.Maxhp);
if (this.GetComponent<HealthSlider>())
{
this.GetComponent<HealthSlider>().SeeTime = 3;
}
}
private int CurrentDamage()
{
float coredamage = UnityEngine.Random.Range(AttackDate.MaxAttack, AttackDate.MinAttack);
if (Iscricital)
{
coredamage = AttackDate.CriticalMultiplier * (coredamage);
}
return (int)coredamage;
}
public void AddPoint(int a)
{
Exeprience += a;
SwitchLevel();
Debug.Log(CurrentLevel);
}
void SwitchLevel()
{
if(Exeprience>0&&Exeprience<=10)
{
CurrentLevel = 1;
AttackDate.MaxAttack = 30;
AttackDate.MinAttack = 20;
CharacterDate.Defence = 10;
}
if (Exeprience > 10 && Exeprience<=25)
{
CurrentLevel = 2;
AttackDate.MaxAttack = 90;
AttackDate.MinAttack = 80;
CharacterDate.Defence = 10;
}
if (Exeprience > 25 && Exeprience<=45)
{
CurrentLevel = 3;
AttackDate.MaxAttack = 90;
AttackDate.MinAttack = 80;
CharacterDate.Defence = 10;
}
if (Exeprience >45 && Exeprience <= 60)
{
CurrentLevel = 4;
}
}
#endregion
}
我们将ScriptsObject的数据生命为属性,对数据进行保护和处理。
public int CurrentDefence
{
get
{
if (CharacterDate != null)
{
return CharacterDate.Defence;
}
else
return 0;
}set
{
CurrentDefence= value;
}
}
public int Currenthealth
{
get
{
return CharacterDate.Currenthealth;
}set
{
CharacterDate.Currenthealth = value;
}
}public int CurrentLevel
{
get
{
return CharacterDate.Level;
}set
{
CharacterDate.Level= value;
}
}public int Exeprience
{
get
{
return CharacterDate.Experience;
}set
{
CharacterDate.Experience = value;
}
}
当人物发生攻击与被攻击时,我们需要计算伤害和实施伤害。这里为了保持代码逻辑清晰我们将伤害的处理函数全部放到CharacterState中进行处理,在PlayerController和EnemyController中我们只负责调用这些处理函数并自身的状态传入到函数中。
CharacterState attack是攻击者的状态
public void TakeDamag(CharacterState attack)
{
//接受传入的攻击者的伤害
int damage = Math.Max(attack.CurrentDamage() - CharacterDate.Defence,1);
//如果被暴击
//if (Iscricital)
//{
// Anim.SetTrigger("GetHit");
//}
Currenthealth -= damage;
Currenthealth = Math.Max(Currenthealth,0);
if (Currenthealth <= 0)
{
attack.AddPoint(30);
}
UPdateHealthUI?.Invoke(Currenthealth, CharacterDate.Maxhp);
if (this.GetComponent<HealthSlider>())
{
this.GetComponent<HealthSlider>().SeeTime = 3;
}
}
public void TakeDamag(int damage)
{
Currenthealth -= damage-CharacterDate.Defence;
Currenthealth = Math.Max(Currenthealth, 0);
UPdateHealthUI?.Invoke(Currenthealth, CharacterDate.Maxhp);
if (this.GetComponent<HealthSlider>())
{
this.GetComponent<HealthSlider>().SeeTime = 3;
}}
private int CurrentDamage()
{
float coredamage = UnityEngine.Random.Range(AttackDate.MaxAttack, AttackDate.MinAttack);
if (Iscricital)
{
coredamage = AttackDate.CriticalMultiplier * (coredamage);
}
return (int)coredamage;
}
我们要明确一下逻辑:谁受到攻击谁进行伤害处理函数的调用,攻击者只需传入自身的状态进行伤害数值的计算即可。
void Hit()
{
if (AttactTarget != null)
{
if (transform.IsFaceTarget(AttactTarget))
{
Debug.Log("在攻击里面");
var enemystate = AttactTarget.GetComponent<CharacterState>();
enemystate.TakeDamag(EnemyState);
}
}
}
攻击函数,我们在动画帧事件中有攻击的发起者进行调用 。
游戏框架6----------->Gamemanger
游戏中存着影响全局事情,比如玩家死后所有的敌人将庆祝胜利。这种控制全局对象我们一般使用GameController来控制,我们将其变为单例模式,方便后期调用。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cinemachine;
public class GameManager :Singleton<GameManager>
{
[HideInInspector]
public CharacterState Playerstate;
public GameObject PlayerPrefab;
List<IEndGameObserve> enemylsit = new List<IEndGameObserve>();
private CinemachineFreeLook cam;
protected override void Awake()
{
base.Awake();
DontDestroyOnLoad(this);
Debug.Log("GameManger"+Time.frameCount);
}
private void OnEnable()
{
}
private void Start()
{
Instantiate(PlayerPrefab, transform.position, Quaternion.identity);
}
public void Rigisterplayer(CharacterState state)
{
Playerstate = state;
cam = GameObject.FindObjectOfType<CinemachineFreeLook>();
if (cam != null)
{
cam.Follow = state.gameObject.transform.GetChild(2);
cam.LookAt= state.gameObject.transform.GetChild(2);
}
}
public void AddObserver(IEndGameObserve observer)
{
enemylsit.Add(observer);
}
public void ReMoveObserver(IEndGameObserve observer)
{
enemylsit.Remove(observer);
}
public void NotifyObservers()
{
foreach (var item in enemylsit)
{
item.EndNotify();
}
}
}
游戏初始化,场景中生成玩家,我们将玩家以变量的形式储存在我们的GameController中,同时也将所有的敌人也储存在脚本中。
List<IEndGameObserve> enemylsit = new List<IEndGameObserve>();
public void AddObserver(IEndGameObserve observer)
{
enemylsit.Add(observer);
}
public void ReMoveObserver(IEndGameObserve observer)
{
enemylsit.Remove(observer);
}玩家死亡时所有的敌人广播
public void NotifyObservers()
{
foreach (var item in enemylsit)
{
item.EndNotify();
}
}
我们在全局进行敌人注册的广播,在每个EnemyController中进行接收。每个敌人继承接口IEndGameObserve,统一实现敌人胜利的方法。当玩家死亡时我通过GameController让所有的敌人广播胜利方法。
if(characterState.Currenthealth==0)
{
GameManager.Instance.NotifyObservers();
Anim.SetBool("Dead",true);
IsDead = true;
}
游戏框架7----------->Savemanger
在游戏中我们实现了传送门的效果。我们可以通过传送门进行切换人物所在的位置甚至也可以对场景进行切换。对于同场景传送而言比较简单,仅仅知识切换人物的位置而已,但是对于跨场景切换我们还要考虑到数据的加载以及场景的加载。
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.AI;
public class SceneController : Singleton<SceneController>
{
public GameObject gameObjectprefab;
protected override void Awake()
{
base.Awake();
DontDestroyOnLoad(this);
}
GameObject Player;
public void TransitionToDestination(PortalPonit enter)
{
switch (enter.Type)
{
case PortalPonit.TransitionType.Samescene:
StartCoroutine(Transiton(SceneManager.GetActiveScene().name,enter.PortalTag));
break;
case PortalPonit.TransitionType.Differnetscene:
StartCoroutine(Transiton(enter.Scenename, enter.PortalTag));
break;
}
}
IEnumerator Transiton(string name,PortalDestination.TransitonTag tag)
{
if (SceneManager.GetActiveScene().name != name)
{
yield return SceneManager.LoadSceneAsync(name);
yield return Instantiate(gameObjectprefab, GetTag(tag).transform.position, GetTag(tag).transform.rotation);
yield break;
}
Player = GameManager.Instance.Playerstate.gameObject;
Player.transform.SetPositionAndRotation(GetTag(tag).transform.position, GetTag(tag).transform.rotation);
Player.GetComponent<NavMeshAgent>().destination = GetTag(tag).transform.position;
yield return null;
}
private GameObject GetTag(PortalDestination.TransitonTag tag)
{
var points = GameObject.FindObjectsOfType<PortalDestination>();
foreach (var item in points)
{
if (item.GetComponent<PortalDestination>().Tag == tag)
{
return item.gameObject;
}
}
return null;
}
}
对于同场景切换我们只需要设置切换的位置即可,我们了创建PortalPoint传送起点类和PortalDestination终点两个类。在SceneManger中我们声明了传输的方法,只需要在起点类中进行调用即可。
public void TransitionToDestination(PortalPonit enter)
{
switch (enter.Type)
{
case PortalPonit.TransitionType.Samescene:
StartCoroutine(Transiton(SceneManager.GetActiveScene().name,enter.PortalTag));
break;
case PortalPonit.TransitionType.Differnetscene:
StartCoroutine(Transiton(enter.Scenename, enter.PortalTag));
break;
}}
IEnumerator Transiton(string name,PortalDestination.TransitonTag tag)
{ 如果跨场景时才需要协程,因为场景的异步加载与主线程无关,我们需要在协程判断异步加载是否完成。
if (SceneManager.GetActiveScene().name != name)
{
yield return SceneManager.LoadSceneAsync(name);
yield return Instantiate(gameObjectprefab, GetTag(tag).transform.position, GetTag(tag).transform.rotation);当上面的程序执行完毕后才会执行break,此时可以推出携程
yield break;
}
Player = GameManager.Instance.Playerstate.gameObject;
Player.transform.SetPositionAndRotation(GetTag(tag).transform.position, GetTag(tag).transform.rotation);
Player.GetComponent<NavMeshAgent>().destination = GetTag(tag).transform.position;
yield return null;
}private GameObject GetTag(PortalDestination.TransitonTag tag)
{
var points = GameObject.FindObjectsOfType<PortalDestination>();
foreach (var item in points)
{
if (item.GetComponent<PortalDestination>().Tag == tag)
{
return item.gameObject;
}
}
return null;
}
后面还有一些细节等待完善,所以未完待续!