Unity开发一个FPS游戏之三

在前面的两篇博客中,我已实现了一个FPS游戏的大部分功能,包括了第一人称的主角运动控制,武器射击以及敌人的智能行为。这里我将继续完善这个游戏,包括以下几个方面:

  1. 增加一个真实的游戏场景,模拟一个废弃的工厂环境
  2. 完善NPC的智能行为,增加巡逻,警戒,躲避,战术编队等行为。
  3. 增加子弹与场景物体交互的效果。
  4. 游戏道具的拾取。
  5. 完善UI界面,增加场景的任务描述,任务完成或失败的判定展示。

以下是完成后的演示。

mission_critical

游戏场景设计

这是一个很大的课题,暂时不是我关注的重点,为此我先直接采用Unity商店的一个免费的资源,RPG/FPS Game Assets for PC/Mobile (Industrial Set v2.0) | 3D 工业场景 | Unity Asset Store。

NPC智能行为

在之前的博客,已经实现了NPC的部分智能行为,包括了随机搜索以及跟踪玩家的行为。在这里,我将添加巡逻,警戒,躲藏、战术编队等行为。

巡逻警戒

首先是实现巡逻功能,这个可以通过定义巡逻路线经过的关键点,然后利用Unity自带的寻路功能来实现。为此,我们需要对场景的道路进行烘培,点击Window->AI->Navigation,然后选择Bake选项,对场景的可导航路线进行烘培。烘培完成后,需要给Enemy这个预制件增加一个NavMesh Agent的组件,然后在代码中就可以指定目标点的坐标,让Agent自己导航到目的地。

其次是增加警戒模式,这个模式比较简单,就是传入一个警戒点的坐标,然后敌人就在警戒点不定期的随机搜索四周。以下是对WanderingAI的代码进行修改,增加巡逻和警戒这两个行为:

[Header("Enemy behavior")]
...
public bool patrolBehavior = false;
public Vector3[] patrolPoints = null;
public bool guardBehavior = false;
public float guardAngle = 60f;
public float guardMaxRotatePeriod = 10.0f;
public float initialGuardAngle = 0f;private long _guardTS = 0;
private float _guardChangeTime = 0;[Flags]
private enum EnemyStatus {  ...Patrol,Guard
}void Start()
{_animator = GetComponent<Animator>();muzzleEffect = GetComponent<MuzzleEffect>();_agent = GetComponent<NavMeshAgent>();currentAmmo = ammo;if (patrolBehavior && patrolPoints.Length >= 2) {status = EnemyStatus.Patrol;_agent.autoBraking = false;_agent.destination = patrolPoints[destPoint];transform.forward = patrolPoints[destPoint] - patrolPoints[0];_animator.SetTrigger("E_Walk");} else if (guardBehavior) {status = EnemyStatus.Guard;transform.localEulerAngles = new Vector3(0f, initialGuardAngle, 0f);_guardTS = DateTime.UtcNow.Ticks;_guardChangeTime = UnityEngine.Random.Range(1.0f, guardMaxRotatePeriod);_animator.SetTrigger("E_Guard");} else {status = EnemyStatus.Idle;}
}// Update is called once per frame
void Update()
{        if (status == EnemyStatus.AimLeft || status == EnemyStatus.AimRight) {AimMove();} else if (status == EnemyStatus.Reload || status == EnemyStatus.Death) {return;} else if (status == EnemyStatus.Aim) {TurnToPlayer();} else if (status == EnemyStatus.Hide) {Hide();} else if (status == EnemyStatus.Damage) {TurnToDamage();if (!_animator.GetBool("E_IsDamage")) {status = EnemyStatus.Idle;prevPlayerPosition = new Vector3(100f, 100f, 100f);}} else {if (status == EnemyStatus.Walk) {Walk();}if (status == EnemyStatus.Sprint) {Sprint();}if (status == EnemyStatus.Patrol) {Patrol();}if (status == EnemyStatus.Guard) {Guard();}DetectPlayer();}
}void Patrol() {if (!_agent.pathPending && _agent.remainingDistance < 0.5f) {Vector3 prevPosition = patrolPoints[destPoint];destPoint = (destPoint + 1) % patrolPoints.Length;_agent.destination = patrolPoints[destPoint];transform.forward = patrolPoints[destPoint] - prevPosition;}
}public void SetPatrolPoints(Vector3[] points) {patrolPoints = points;
}private void Guard() {float interval = (DateTime.UtcNow.Ticks - _guardTS)/10000000.0f;if (interval >= _guardChangeTime) {_guardTS = DateTime.UtcNow.Ticks;_guardChangeTime = UnityEngine.Random.Range(1.0f, guardMaxRotatePeriod);float rotateAngle = UnityEngine.Random.Range(-guardAngle, guardAngle);transform.localEulerAngles = new Vector3(0f, initialGuardAngle + rotateAngle, 0f);}
}

从以上代码可以看到,要设定敌人的行为是巡逻,需要传入巡逻点的坐标,然后依次把巡逻点的坐标设置为Agent的目的地即可,在巡逻的过程中,敌人同样需要检测玩家,如果发现玩家则改变为瞄准行为,这个和之前的随机搜索的行为切换是保持一致的。当行为是警戒时,将随机不定时的转动一些角度来探测玩家。

装弹躲藏

进一步改进敌人在交战时的行为,当敌人打完子弹时,现在的行为是站定了换子弹,为了提高难度,我们可以设计当敌人打完子弹,自动跑到一些隐藏点来重新装弹,之后再出来和玩家交战。为此我们需要设计一些隐藏点。我采取的方法是在场景中选取一些隐藏点,在每个地点放置一个小的正方体Game Object,然后在游戏装载场景时,读取这些Game Object的坐标,然后把这些Object都设置为SetActive(false)进行隐藏,把这些坐标传给敌人,然后当需要跑去隐藏点时,判断距离最近并且不被玩家发现的隐藏点并自动导航。代码改动如下:

private Vector3[] _hidePoints;private enum EnemyStatus {  ...Hide
}public void SetHidePoints(List<Vector3> points) {_hidePoints = points.ToArray();
}private void Shoot() {if (currentAmmo==0) {Hide();return;}...
}private Vector3 FindNearestHidePoint() {GameObject player = GameObject.Find("Player");Vector3 playerPosition = player.transform.position;float distance = 0f;Vector3 selectedPosition = Vector3.up;RaycastHit info = new RaycastHit();int layermask = LayerMask.GetMask("Character", "Default");for (int i=0;i<_hidePoints.Length;i++) {Vector3 hidePosition = _hidePoints[i];float tempDistance = (transform.position - hidePosition).magnitude;if (tempDistance > eyeviewDistance) {continue;} else {float playerDistance = (playerPosition - hidePosition).magnitude;if (tempDistance > playerDistance) {continue;} else {Physics.Raycast(hidePosition+Vector3.up, playerPosition - hidePosition - Vector3.up, out info, eyeviewDistance, layermask);if (info.collider.name == "Player") {continue;} else {if (distance == 0 || tempDistance < distance) {distance = tempDistance;selectedPosition = hidePosition;}}}}}return selectedPosition;
}private void Hide() {if (status == EnemyStatus.Hide) {if (!_agent.pathPending && _agent.remainingDistance < 0.1f) {_agent.isStopped = true;if (currentAmmo == 0) {status = EnemyStatus.Reload;corReload = StartCoroutine(Reload());}} else {return;}} else {Vector3 hidePoint = FindNearestHidePoint();if (hidePoint != Vector3.up) {transform.forward = hidePoint - transform.position;_agent.isStopped = false;_agent.destination = hidePoint;_agent.speed = sprintSpeed;status = EnemyStatus.Hide;_animator.SetTrigger("E_Sprint");} else {status = EnemyStatus.Reload;corReload = StartCoroutine(Reload());}}
}

听觉识别

当射击开火时,可以设置在某个范围内的敌人会识别声音的方向,并跑到交火的位置附近进行增援。

修改WanderingAI的代码,增加一个方法

public void HearShoot(Vector3 position) {if (status == EnemyStatus.Walk || status == EnemyStatus.Idle || status == EnemyStatus.Patrol) {status = EnemyStatus.Sprint;_agent.destination = position;_agent.isStopped = false;_animator.SetTrigger("E_Sprint");_agent.updateRotation = true;}
}

然后当玩家开火时,我们可以检测在开火的某个范围内,是否有敌人存在,如果有,则调用敌人的WanderingAI的HearShoot方法,把玩家当前的坐标通知给敌人,如果敌人当前处在空闲,随机搜索或巡逻状态时,则会切换为快跑状态,快速跑到玩家位置进行攻击。

修改玩家的PlayerController的代码,在Shoot函数中增加以下部分:

[Header("Notify enemy")]
public float soundDistance = 30.0f;private void Shoot() {if (_input.shoot && _currentBulletVolume>0) {if (!_animator.GetBool("IsReload")) {long nowTS = DateTime.UtcNow.Ticks;float shootInterval = (nowTS - _input.shootPressTS)/10000000.0f;if (shootInterval >= 0.08 || _input.firstShoot) {_animator.SetTrigger("Shoot");bullet = Instantiate(bulletPrefab, _muzzle.position, _muzzle.rotation);bullet.GetComponent<Rigidbody>().velocity = _muzzle.forward * 800.0f;casing = Instantiate(casingPrefab, _eject.position, _eject.rotation);muzzleEffect.Effect(_muzzle.position);_input.shootPressTS = nowTS;_input.firstShoot = false;_currentBulletVolume--;ExecuteEvents.Execute<IGameMessage>(_gameManager, null, (x,y)=>x.ArmoMessage(_currentBulletVolume));// Simulate the enemy can hear the shoot sound_spottedEnemies = Physics.OverlapSphere(transform.position, soundDistance, LayerMask.GetMask("Enemy"));for (int i=0;i<_spottedEnemies.Length;i++) {WanderingAI behavior = _spottedEnemies[i].gameObject.GetComponent<WanderingAI>();behavior.HearShoot(transform.position);}}}  }
}

战术组队

考虑最简单的一种三角形编队,即领队走在最前面,后面另外两个人和领队呈三角形分布。当发现玩家时,即采取包抄的战术。

对WanderingAI代码改动一下,增加和编队相关的几个属性:

[Header("Enemy Role")]
public bool formation = false;
public int formationType = 0;  // formation type 0: triangle
public int roleId = 0; // role 0: leader, 1: left follower, 2: right follower
public float followDistance = 5.0f;  // How far the follower behind the leaderprivate GameObject _leader;
private Vector3 _adjustFormationDistance;
private List<GameObject> _teamMembers = new List<GameObject>();public void SetLeader(GameObject leader) {_leader = leader;
}public void AddMember(GameObject member) {_teamMembers.Add(member);
}

如果设置了formation为true,那么即表示这个NPC处于编队状态,formationType控制不同的编队类型,当前只实现三角形编队,roleID为0表示这个NPC是领队,领队初始行为是随机搜索,其他roleID的NPC是队员,需要跟着领队,因此设置了一个followDistance来设置其跟随领队的距离。_adjustFormationDistance用于根据followDistance来生成一个Vector3向量,进行距离调整。如果NPC是领队,那么需要保存队员的GameObject到一个list中,如果NPC是队员,那么需要保存领队的GameObject,这样可以方便互相之间的通讯。

增加一个新的状态值,用于表示队员NPC是否处于跟随队长的状态:

[Flags]
private enum EnemyStatus {  ...FollowLeader
}

在Start函数中,对formation状态进行初始化设置

if (formation) {status = EnemyStatus.Walk;_animator.SetTrigger("E_Walk");_adjustFormationDistance = new Vector3(followDistance, 0f, 0f);
}

初始设置编队时,我们可以通过初始化NPC的坐标来保持三角形编队,但是当后续游戏进行时,如果队员遇到障碍物时,需要避让障碍物之后仍能保持队形。我采取一个简便的方法来解决这个问题,那就是让队员遇到障碍物时去到队长当前的位置,而不是随机转向,之后再重新调整队员位置,使得能维持队形。

重新修改Walk方法,并新增一个FollowLeader的方法,代码如下:

void Walk() {if (sprintTS != 0) {long nowTS = DateTime.UtcNow.Ticks;float interval = (nowTS - sprintTS)/10000000.0f;if (interval >= sprintSearchPeriod) {sprintTS = 0;}}float distance = DetectObstacle(transform.forward);if (distance < obstacleRange) {// For NPC, if not the leader, should not random rotate, need to follow to the current leader position.if (formation && roleId != 0 && _leader) {_agent.destination = _leader.transform.position;_agent.isStopped = false;status = EnemyStatus.FollowLeader;return;} else {float angle;if (sprintTS == 0) {angle = UnityEngine.Random.Range(-randomAngle, randomAngle);} else {angle = UnityEngine.Random.Range(-sprintSearchAngle, sprintSearchAngle);}transform.Rotate(0, angle, 0);}}if (formation && roleId != 0 && _leader) {float angle = Vector3.Angle(_leader.transform.forward, _leader.transform.position - transform.position);if (angle >= 30) {transform.forward = _leader.transform.forward;}}transform.Translate(0, 0, speed * Time.deltaTime);
}void FollowLeader() {if (_agent.remainingDistance < 0.5f) {_agent.isStopped = true;randomSearch = true;_animator.SetTrigger("E_Walk");status = EnemyStatus.Walk;// ReformationVector3 direction;if (roleId == 1) {direction = _leader.transform.position - _adjustFormationDistance - transform.position;} else {direction = _leader.transform.position + _adjustFormationDistance - transform.position;}transform.forward = direction;} else {FaceTarget();}
}

当队员遇到障碍时,设置状态为FollowLeader,并且通过NavmeshAgent自动导航到队长当前位置。当处于FollowLeader状态时,如果接近队长位置,则重新调整方向,设置状态为Walk,然后在Walk方法中判断队员与队长的方向的夹角和距离,如果满足条件则重新调整方向,使得重新维持队形不变。

最后就是考虑到当队长NPC挂掉时,要通知队员解散编队,为此定义一个Deformation方法

public void DeFormation() {foreach (GameObject _member in _teamMembers) {if (_member) {WanderingAI _behavior = _member.GetComponent<WanderingAI>();_behavior.formation = false;_behavior.roleId = 0;}}
}

考虑到当前游戏场景的位置比较狭小,不方便队员在发现玩家时进行包抄,所以暂时不添加更多的战术功能,只实现一个编队搜索。

子弹与场景物体的交互

为了使游戏更真实,我们可以让子弹在射击到水泥墙面或者金属物体时产生碰撞效果和留下弹痕,当射击到油罐之类的物体时,可以引爆油罐。

下面来实现子弹撞击物体的效果。

模拟灰尘溅射

首先是模拟子弹撞击水泥物体之后产生的灰尘溅射。在Asset的Texture目录下导入一张灰尘的图片,设置里面要勾选Alpha is Transparancy。例如下图:

在Asset的Material目录下,新建一个material文件,命名为M_Smoke。Shader选择Particles/Standard Surface,Rendering mode选择Fade。Main Option勾选Two Sided和Camera Fading。把刚才导入的材质贴图拖动到Maps的Albedo贴图中。这样就定义好材质了。

新建一个空的GameObject,命名为Impact Effect,保存为Prefab,然后进行编辑。

新增一个子物体,命名为Concrete Effect,添加一个Particle System的Component,具体设置如下:

模拟弹孔

在Asset的Texture目录导入一张弹孔的图片作为Texture,如下图:

在Asset的Material目录新建一个材质文件,命名为M_Impact_Concrete,shader选择Legacy Shaders/Particles/VertexLit Blended,然后把之前新建的Texture拖动到Particle Texture中。

在Impact Effect这个Prefab下新建一个子物体,命名为Bullet Hole,新建一个Particle System的Component,设置如下:

模拟水泥碎片

同样是找一个水泥碎片的图片导入到Asset的Texture目录中,如下图:

然后同样是新建一个材质,如之前弹孔的操作步骤。

在Impact Effect这个Prefab下新建一个子物体,命名为Concrete Debris,添加Particle system,配置如下:

 

代码实现

最后就是在Impact Effect这个Prefab上新增一个脚本代码,播放声音以及控制多长时间销毁,代码如下:

public class Impact : MonoBehaviour
{[Header("Impact Despawn Timer")]//How long before the impact is destroyedpublic float despawnTimer = 10.0f;[Header("Audio")]public AudioClip[] impactSounds;public AudioSource audioSource;// Start is called before the first frame updatevoid Start(){StartCoroutine (DespawnTimer ());//Get a random impact sound from the arrayaudioSource.clip = impactSounds[Random.Range(0, impactSounds.Length)];//Play the random impact soundaudioSource.Play();}// Update is called once per framevoid Update(){}private IEnumerator DespawnTimer() {//Wait for set amount of timeyield return new WaitForSeconds (despawnTimer);//Destroy the impact gameobjectDestroy (gameObject);}
}

编辑Bullet的脚本文件,增加以下代码:

...
[Header("Impact Effect Prefabs")]
public Transform []	concreteImpactPrefabs;private void OnCollisionEnter(Collision collision) {...if (collision.gameObject.CompareTag("Concrete")) {//Instantiate random impact prefab from arrayInstantiate (concreteImpactPrefabs [0], transform.position, Quaternion.LookRotation (collision.contacts [0].normal));//Destroy bullet objectDestroy(gameObject);}...
}

然后我们把场景中的水泥墙的预制件的Tag都设置为Concrete,这样当子弹发射碰撞到水泥墙时,就能看到相应的效果了。

实现爆炸效果

当子弹打中油桶之类的物体时,可以触发爆炸效果。

首先需要制作油桶爆炸后分成的碎块效果,可以通过Blender来实现。

把模型导入到Blender,然后安装Cell Fracture插件,选择物体->快速效果->Cell Fracture,调整Souce Limit的数值,这个表示物体破碎为多少块,这样插件就会自动进行随机的破碎,然后我们只保留破碎后的物体即可,导出为FBX格式。如下图所示:

由于油桶为空心物体,现在这样制作的爆炸碎片是实心的,为了进一步改进,我们可以复制多一个油桶物体,稍微缩小一点,然后放置在原来油桶里面,通过Blender的Object Bool Tool插件来进行布尔差值切割,这样就能制作一个空心的油桶了,然后再进行Cell Fracture,效果更好。

把导出的模型添加到Assets->Models目录,然后拖动到场景,再拖动到Assets->Prefabs目录,保存为一个Prefab。需要注意,这个Prefab下的所有碎片子物体,需要全部添加Rigidbody组件,并且不要勾选Kinematic。如果我们希望破碎后保留这些碎片,那么还需要增加Mesh Collider组件并勾选Vertex,不然的话破碎之后时看不到这些碎片的。

为了实现爆炸的粒子效果,我们可以下载Unity Asset Store里面的Particle Pack插件,里面提供了很多效果的粒子系统实现,非常好用。

对于原始的未爆炸的油桶Prefab,为其新增一个Explode的Tag,并且添加一个名为Debris的脚本文件,代码如下:

public class Debris : MonoBehaviour
{public GameObject Explosive_debris;// Start is called before the first frame updatevoid Start(){}// Update is called once per framevoid Update(){}public void OnDestroy(){GameObject o = Instantiate(Explosive_debris, transform.position, transform.rotation);o.GetComponent<Explode>().Explodsion();Destroy(gameObject);}
}

然后把我们刚才制作的爆炸后的油桶Prefab拖动到这个Script的Explosive_debris中。

在新的爆炸后的油桶Prefab上新增一个名为Explode的脚本文件,代码如下:

public class Explode : MonoBehaviour
{public GameObject explodeEffect;public float force = 500f;public float radius = 5f;public float destroyAfter = 2f;// Start is called before the first frame updatevoid Start(){}// Update is called once per framevoid Update(){}public void Explodsion() {StartCoroutine(PlayExplodeEffect ());var rbs = GetComponentsInChildren<Rigidbody>();foreach(var rb in rbs) {rb.AddExplosionForce(1000f, transform.position, 10f);}}private IEnumerator PlayExplodeEffect () {GameObject o = Instantiate(explodeEffect, transform.position, transform.rotation);yield return new WaitForSeconds (destroyAfter);Destroy(o);}
}

 然后把Particle Pack的BigExplosion拖动到这个Script的explosionEffect中。

最后就是修改一下Bullet的脚本文件,代码如下:

private void OnCollisionEnter(Collision collision) {...if (collision.gameObject.CompareTag("Explode")) {Debris dest = collision.gameObject.GetComponent<Debris>();if (dest != null) {dest.OnDestroy();}}Destroy(this.gameObject);
}

现在爆炸的效果就制作好了。 

游戏道具的拾取

为了增加游戏的趣味性,需要设计一些游戏道具,例如医药包,弹药等。

从网上找一个白色十字的3D模型作为医药包,导入到Assets->Models目录,然后保存为Prefab。

编辑这个Prefab,添加Rigidbody和Box Collider组件,其中Rigidbody需要勾选Is Kinematic,Box Collider需要勾选Is Trigger。

在Assets->Scripts目录新增一个Pickup的代码文件,代码如下:

[RequireComponent(typeof(Rigidbody), typeof(Collider))]
public class Pickup : MonoBehaviour
{[Tooltip("Frequency at which the item will move up and down")]public float VerticalBobFrequency = 1f;[Tooltip("Distance the item will move up and down")]public float BobbingAmount = 1f;[Tooltip("Rotation angle per second")] public float RotatingSpeed = 360f;[Tooltip("Sound played on pickup")] public AudioClip PickupSfx;[Tooltip("VFX spawned on pickup")] public GameObject PickupVfxPrefab;public Rigidbody PickupRigidbody { get; private set; }Collider m_Collider;Vector3 m_StartPosition;bool m_HasPlayedFeedback;protected virtual void Start(){PickupRigidbody = GetComponent<Rigidbody>();m_Collider = GetComponent<Collider>();// ensure the physics setup is a kinematic rigidbody triggerPickupRigidbody.isKinematic = true;m_Collider.isTrigger = true;// Remember start position for animationm_StartPosition = transform.position;}void Update(){// Handle bobbingfloat bobbingAnimationPhase = ((Mathf.Sin(Time.time * VerticalBobFrequency) * 0.5f) + 0.5f) * BobbingAmount;transform.position = m_StartPosition + Vector3.up * bobbingAnimationPhase;// Handle rotatingtransform.Rotate(Vector3.up, RotatingSpeed * Time.deltaTime, Space.Self);}void OnTriggerEnter(Collider other){PlayerController pickingPlayer = other.GetComponent<PlayerController>();if (pickingPlayer != null){OnPicked(pickingPlayer);}}protected virtual void OnPicked(PlayerController playerController){PlayPickupFeedback();}public void PlayPickupFeedback(){if (m_HasPlayedFeedback)return;if (PickupSfx){//AudioUtility.CreateSFX(PickupSfx, transform.position, AudioUtility.AudioGroups.Pickup, 0f);}if (PickupVfxPrefab){var pickupVfxInstance = Instantiate(PickupVfxPrefab, transform.position, Quaternion.identity);}m_HasPlayedFeedback = true;}
}

这个代码定义了所有道具的基本Pickup行为

对于医药包道具的Pickup,除了基本行为之外,还有个特殊的行为就是给玩家加血,因此定义一个HealthPickup的代码,如下:

public class HealthPickup : Pickup
{[Header("Parameters")] [Tooltip("Amount of health to heal on pickup")]public int HealAmount = 2;protected override void OnPicked(PlayerController player) {player.Heal(HealAmount);Destroy(gameObject);}
}

把这个代码添加到医药包Prefab上。

当玩家触碰到医药包时,就会回调PlayerController脚本的Heal方法,因此添加一个Heal方法,代码如下:

public void Heal(int point) {if (_health+point >= PlayerHealth) {_health = PlayerHealth;} else {_health += point;}float healthValue = (float) _health/PlayerHealth;ExecuteEvents.Execute<IGameMessage>(_gameManager, null, (x,y)=>x.HealthMessage(healthValue));
}

之后我们可以实现一个木箱子的破碎效果,当木箱子被打破后,就会露出医药包。具体定义木箱子破碎的过程和之前的类似,只是破碎效果不要选择爆炸,而是DustExplodsion。

完善UI界面

任务简介

最后就是完善整个游戏的UI界面,在游戏一开始增加任务介绍,当任务完成或失败时展示相应的界面。

首先是增加任务简介,在GameScreen这个Prefab下新建一个Text TMP,命名为Task,然后输入任务简介。编辑GameLevelLoader脚本文件,增加代码如下:

...
private long _startTick = 0;
private GameObject _task;void Start()
{..._startTick = DateTime.UtcNow.Ticks;_task = GameObject.Find("GameScreen/Task");
}void Update()
{long nowTS = DateTime.UtcNow.Ticks;float interval = (nowTS - _startTick)/10000000.0f;if (interval >= 10f) {if (_task) {_task.SetActive(false);}}
}

这个代码表示在游戏开始的头10秒显示这一关任务的简介。

任务完成或失败的场景显示

当这一关任务完成或失败时,应该显示对应的场景。

在Assets->Scenes的目录下,新建两个场景,分别命名为WinScene和LoseScene,每个场景都增加一个Canvas,然后在Canvas下新增一个背景图片,一个文字描述和一个按钮,如下图所示:

定义一个消息接口文件GameFlowMessage,用来传递设定场景任务以及消灭敌人等消息的传递,如以下代码:

public interface IGameFlowMessage : IEventSystemHandler
{void KillEnemyMessage();void FindTargetMessage();void SetObjectiveMessage(int killEnemyNumber, int findTargetNumber);void PlayerDeathMessage();    
}

 然后定义一个GameFlowController脚本文件,实现以上接口的方法,如以下代码:

public class GameFlowController : MonoBehaviour, IGameFlowMessage
{[Header("Win")] [Tooltip("This string has to be the name of the scene you want to load when winning")]public string WinSceneName = "WinScene";[Header("Lose")] [Tooltip("This string has to be the name of the scene you want to load when winning")]public string LoseSceneName = "LoseScene";private int _killEnemyNumber;private int _currentKillEnemy = 0;private int _findTargetNumber;private int _currentFindTarget = 0;public void KillEnemyMessage() {_currentKillEnemy++;Debug.LogFormat("Kill Enemy, current:{0}, target:{1}", _currentKillEnemy, _killEnemyNumber);if (_currentKillEnemy == _killEnemyNumber && _currentFindTarget == _findTargetNumber) {WinOrLose(true);}}public void FindTargetMessage() {_currentFindTarget++;if (_currentKillEnemy == _killEnemyNumber && _currentFindTarget == _findTargetNumber) {WinOrLose(true);}}public void SetObjectiveMessage(int killEnemyNumber, int findTargetNumber) {_killEnemyNumber = killEnemyNumber;_findTargetNumber = findTargetNumber;}public void PlayerDeathMessage() {WinOrLose(false);}public void WinOrLose(bool win) {Cursor.lockState = CursorLockMode.None;Cursor.visible = true;if (win) {SceneManager.LoadScene(WinSceneName);} else {SceneManager.LoadScene(LoseSceneName);}}
}

把这个Script加到GameLoader这个GameObject上面,然后编辑GameLevelLoader脚本,设定这一关任务,代码如下:

...
private GameFlowController _gameFlowController;void Start()
{..._gameFlowController = GetComponent<GameFlowController>();_gameFlowController.SetObjectiveMessage(9, 0);
}

修改NPC的WanderingAI代码,当NPC被消灭时,发送KillEnemyMessage,代码如下:

...
private GameObject _gameLoader;void Start()
{..._gameLoader = GameObject.Find("GameLoader");
}private IEnumerator Death() {...ExecuteEvents.Execute<IGameFlowMessage>(_gameLoader, null, (x,y)=>x.KillEnemyMessage());
}

总结

以上就是一个FPS游戏的制作过程,后续将继续完善玩家的换武器,以及关卡的设计。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/799984.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

谷粒商城实战(011 业务-检索服务)

Java项目《谷粒商城》架构师级Java项目实战&#xff0c;对标阿里P6-P7&#xff0c;全网最强 总时长 104:45:00 共408P 此文章包含第173p-第p194的内容 介绍 这些过滤条件都可以写在must里&#xff0c;但是filter不参与评分&#xff0c;速度会快一些&#xff0c;所以一些分类等…

AssetBundle打包

AssetBundle技术的概念 Unity的AssetBundle是一个资源压缩包&#xff0c;包含模型、贴图、预制体、声音甚至整个场景&#xff0c;可以在游戏运行时被加载。   AssetBundle自身保存着相互的依赖关系&#xff0c;压缩包可以使用LZMA和LZ4压缩算法&#xff0c;减少包大小&#x…

【学习】移动端App性能测试流程有哪些

移动端App性能测试是保证App性能表现的重要环节之一。随着移动设备的普及和移动互联网的发展&#xff0c;移动端App的性能测试变得越来越重要&#xff0c;通过科学合理的性能测试可以发现并解决潜在的性能问题优化App运行效果提高用户体验。性能测试旨在评估App在各种场景下的性…

基于视频监管与AI智能识别技术的水利河道综合治理解决方案

一、方案介绍 TSINGSEE青犀视频水利河道综合治理解决方案是依托视频AI智能分析技术&#xff0c;利用水质/水文等传感器、高清摄像机、水利球、无人机、无人船等感知设备实时采集数据&#xff0c;并与视频能力进行联动&#xff0c;达到智能预警的目的。 TSINGSEE青犀方案以信息…

Spring Cloud微服务入门(三)

服务注册与发现的概念 服务之间相互访问&#xff1a; 例如&#xff1a;用户中心与内容中心之间相互调用。 问题&#xff1a; 服务调用需要知道对方的服务地址&#xff0c;地址写在哪里&#xff1f; 如果服务是多个实例部署&#xff0c;该调用哪一个&#xff1f; 如果服务是多…

Jetpack Compose -> 状态机制的背后秘密

前言 上一章我们讲解了 Jetpack Compose 的无状态、状态提升、单向数据流 本章我们讲解下状态机制的背后秘密 List 前面我们讲过&#xff0c;通过 by mustableStateOf() 就可以被 Compose 自动订阅了&#xff1b;我们前面是通过 String 类型进行的自动订阅&#xff0c;那么换成…

【深度学习】YOLO-World: Real-Time Open-Vocabulary Object Detection,目标检测

介绍一个酷炫的目标检测方式&#xff1a; 论文&#xff1a;https://arxiv.org/abs/2401.17270 代码&#xff1a;https://github.com/AILab-CVC/YOLO-World 文章目录 摘要Introduction第2章 相关工作2.1 传统目标检测2.2 开放词汇目标检测 第3章 方法3.1 预训练公式&#xff1a…

电容隔离型±10V输入隔离放大器特点:ISOC 124P

产品特点: 50KHz(-3dB)高带宽与ISO 124P隔离器Pin-Pin兼容 低成本小体积&#xff0c;标准DIP16Pin阻燃材料封装 精度等级:0.01级&#xff0c;全量程内非线性度0.01% 信号输入与输出之间:3000VDC隔离耐压 电源范围:4.5V~18V 双极运算:Vo10V 方便易用&#xff0c;固定单位增益配置…

ubuntu安装nginx以及开启文件服务器

1. 下载源码 下载页面&#xff1a;https://nginx.org/en/download.html 下载地址&#xff1a;https://nginx.org/download/nginx-1.24.0.tar.gz curl -O https://nginx.org/download/nginx-1.24.0.tar.gz2. 依赖配置 sudo apt install gcc make libpcre3-dev zlib1g-dev ope…

【分治算法】Strassen矩阵乘法Python实现

文章目录 [toc]问题描述基础算法时间复杂性 Strassen算法时间复杂性 问题时间复杂性Python实现 个人主页&#xff1a;丷从心. 系列专栏&#xff1a;Python基础 学习指南&#xff1a;Python学习指南 问题描述 设 A A A和 B B B是两个 n n n \times n nn矩阵&#xff0c; A A…

CICD流水线 发布应用到docker镜像仓库

准备工作 1.先注册免费的镜像仓库 复制链接: https://cr.console.aliyun.com/cn-beijing/instances 实施 1. 新建流水线&#xff0c;选择模板 2.添加流水线源&#xff0c;及是你的代码仓库, 选择对应分支. 3.代码检查以及单元测试&#xff0c;这个步骤可以不用动它. 4. …

AI的力量感受(附网址)

输入 科技感的 二维码&#xff0c;生成如下&#xff0c;还是可以的 输入金属感 的芯片&#xff0c;效果就很好了 金属感 打印机&#xff0c;细节丰富&#xff0c;丁达尔效应 就有点跑题了 金属感 扫码仪 还有点像 3D 封装长这样&#xff0c;跑题比较严重 总之&#xff0c;AI还…

如何使用生成式人工智能撰写关于新产品发布的文章?

利用生成式人工智能撰写新产品发布文章确实是一种既有创意又高效的内容生成方式。以下是如何做到这一点的指南&#xff0c;附带一些背景信息&#xff1a; • 背景&#xff1a;在撰写文章之前&#xff0c;收集有关您的新产品的信息。这包括产品的名称、类别、特点、优势、目标受…

朗汀留学美国生物医学工程专业留学部分录取案例合集

满怀期待的憧憬与金榜题名的喜悦交织着未来的掌声&#xff0c;捧在手心里的不仅仅是一份一份努力浇灌的录取通知&#xff0c;更是一起拼搏走过的岁月沉淀。 我们感恩每一位朗汀留学的学生和家长&#xff0c;是你们的支持与信任&#xff0c;让我们有机会共享此刻的荣耀&#xff…

数据挖掘及其近年来研究热点介绍

&#x1f380;个人主页&#xff1a; https://zhangxiaoshu.blog.csdn.net &#x1f4e2;欢迎大家&#xff1a;关注&#x1f50d;点赞&#x1f44d;评论&#x1f4dd;收藏⭐️&#xff0c;如有错误敬请指正! &#x1f495;未来很长&#xff0c;值得我们全力奔赴更美好的生活&…

jdk目录结构

jdk目录详解 JDK(Java Development Kit&#xff0c;Java开发包&#xff0c;Java开发工具)是一个写Java的applet和应用程序的程序开发环境。它由一个处于操作系统层之上的运行环境还有开发者 编译&#xff0c;调试和运行用Java语言写的applet和应用程序所需的工具组成。 JDK(J…

【数据结构】考研真题攻克与重点知识点剖析 - 第 6 篇:图

前言 本文基础知识部分来自于b站&#xff1a;分享笔记的好人儿的思维导图与王道考研课程&#xff0c;感谢大佬的开源精神&#xff0c;习题来自老师划的重点以及考研真题。此前我尝试了完全使用Python或是结合大语言模型对考研真题进行数据清洗与可视化分析&#xff0c;本人技术…

爱普生语音芯片的特点与应用市场

随着物联网与智能家居的普及&#xff0c;越来越多的电子产品有了语音播报的需求。但是很多客户没有类似的开发经验或者他们的产品内部只能承载一个蜂鸣器。这样的情况下要如何实现快速的产品升级呢?下面让我们来看一下差普生语音芯片是如果帮助客户的。目前爱普生语音芯片分为…

Redis 的主从复制、哨兵和cluster集群

目录 一. Redis 主从复制 1. 介绍 2. 作用 3. 流程 4. 搭建 Redis 主从复制 安装redis 修改 master 的Redis配置文件 修改 slave 的Redis配置文件 验证主从效果 二. Redis 哨兵模式 1. 介绍 2. 原理 3. 哨兵模式的作用 4. 工作流程 4.1 故障转移机制 4.2 主节…

记录一次内网渗透过程

0x01 前言&#xff1a; 一切以学习为主&#xff0c;记录一次小小的攻击过程 本次是通过外网漏洞撕开的口子&#xff0c;主要通过一下方式 拿到了目标资产 nday扫一扫 弱口令爆一爆 上传接口找一找 后台上传找一找 数据库弱口令 关注新day&#xff0c;有了立马在资产里面…