当前位置: 首页 > news >正文

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游戏的制作过程,后续将继续完善玩家的换武器,以及关卡的设计。

相关文章:

Unity开发一个FPS游戏之三

在前面的两篇博客中&#xff0c;我已实现了一个FPS游戏的大部分功能&#xff0c;包括了第一人称的主角运动控制&#xff0c;武器射击以及敌人的智能行为。这里我将继续完善这个游戏&#xff0c;包括以下几个方面&#xff1a; 增加一个真实的游戏场景&#xff0c;模拟一个废弃的…...

NIUSHOP完美运营版商城 虚拟商品全功能商城 全能商城小程序 智慧商城系统 全品类百货商城

完美运营版商城/拼团/团购/秒杀/积分/砍价/实物商品/虚拟商品等全功能商城 干干净净 没有一丝多余收据 还没过手其他站 还没乱七八走的广告和后门 后台可以自由拖曳修改前端UI页面 还支持虚拟商品自动发货等功能 挺不错的一套源码 前端UNIAPP 后端PHP 一键部署版本 源码免费…...

vue2开发好还是vue3开发好vue3.0开发路线

Vue 2和Vue 3都是流行的前端框架&#xff0c;它们各自有一些特点和优势。选择Vue 2还是Vue 3进行开发&#xff0c;主要取决于你的项目需求、团队的技术栈、以及对新特性的需求等因素。以下是一些关于Vue 2和Vue 3的比较&#xff0c;帮助你做出决策&#xff1a; Vue 2&#xff1…...

爬虫 新闻网站 并存储到CSV文件 以红网为例 V2.0 (控制台版)升级自定义查询关键词、时间段,详细注释

爬虫&#xff1a;红网网站&#xff0c; 获取指定关键词与指定时间范围内的新闻&#xff0c;并存储到CSV文件 V2.0&#xff08;控制台版&#xff09; 爬取目的&#xff1a;为了获取某一地区更全面的在红网已发布的宣传新闻稿&#xff0c;同时也让自己的工作更便捷 对比V1.0升级的…...

JavaSE-11笔记【多线程2(+2024新)】

文章目录 6.线程安全6.1 线程安全问题6.2 线程同步机制6.3 关于线程同步的面试题6.3.1 版本16.3.2 版本26.3.3 版本36.3.4 版本4 7.死锁7.1 多线程卖票问题 8.线程通信8.1 wait()和sleep的区别&#xff1f;8.2 两个线程交替输出8.3 三个线程交替输出8.4 线程通信-生产者和消费者…...

WebKit是什么?

WebKit是一个开源的浏览器引擎&#xff0c;它用于呈现网页内容在许多现代浏览器中&#xff0c;包括Safari浏览器、iOS内置浏览器、以及一些其他浏览器如Google Chrome的早期版本。以下是一些关于WebKit的重要信息&#xff1a; 起源和发展&#xff1a;WebKit最初是由苹果公司为其…...

谷歌(Google)历年编程真题——接雨水

谷歌历年面试真题——数组和字符串系列真题练习。 接雨水 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,1,2,1] 输出&#xff1a;…...

golang 归并回源策略

前言 下面是我根据业务需求画了一个架构图&#xff0c;没有特别之处&#xff0c;很普通&#xff0c;都是我们常见的中间件&#xff0c;都是一些幂等性GET 请求。有一个地方很有意思&#xff0c;从service 分别有10000 qps 请求到Redis&#xff0c;并且它们的key 是一样的。这样…...

【漏洞复现】可视化融合指挥调度平台 dispatch接口处存在任意文件上传漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…...

最讨厌这种字符串问题了!!

题目&#xff1a;洛谷P1957口算练习题 题目大意描述&#xff1a; 第一行输入一个整数表示接下来要进行多少次运算&#xff0c;接下来每行输入一个字母c和两个数字x,y&#xff08;输入的字母为a/b/c,分别表示要进行&#xff0c;-&#xff0c;*运算&#xff09;或者就输入两个数…...

B-名牌赌王(本人遇到的题,做个笔记)

题解&#xff1a; #include <iostream> #include <queue> //需要用小根堆的优先队列 #include <unordered_map> //用无序映射 using namespace std; bool pai() {int n, m;cin >> n >> m; priority_queue<int, vector<int>, gr…...

博客评论回复03

接着之前写的&#xff0c;之前返回的数据集按道理来说渲染出来还是丑丑的&#xff0c;因此这次我看着抖音的评论样子&#xff0c;自己瞎写了一通&#xff0c;不过也算是模仿出来了虽然肯定没有抖音写的好。 类似与前面几章写的表结构 首先看看抖音评论区是怎么样的&#xff1f…...

【【萌新的学习之Numpy数组的使用】】

萌新的学习之Numpy数组的使用 先记录一下之前的关于函数的设计 通过创造类的形式 复习完毕之后介绍numpy数组的使用 #整数型数组遇到除法 &#xff08;即便是除以整数&#xff09; 不同维度的数组之间 从外形上的本质区别 一维数组用1层中括号 二维数组用2层中括号 三维数…...

RabbitMQ3.13.x之七_RabbitMQ消息队列模型

RabbitMQ3.13.x之七_RabbitMQ消息队列模型 文章目录 RabbitMQ3.13.x之七_RabbitMQ消息队列模型1. RabbitMQ消息队列模型1. 简单队列2. Work Queues(工作队列)3. Publish/Subscribe(发布/订阅)4. Routing(路由)5. Topics(主题)6. RPC(远程过程调用)7. Publisher Confirms(发布者…...

Android JNI 调用第三方SO

最近一个项目使用了Go 编译了一个so库&#xff0c;但是这个so里面还需要使用第三方so库pdfium, 首先在Android工程把2个so库都放好 在jni中只能使用dlopen方式&#xff0c;其他的使用函数指针的方式来调用&#xff0c;和windows dll类似&#xff0c;不然虽然编译过了但是会崩溃…...

Vid2seq

Vid2Seq 应该是目前为止,个人最中意得一篇能够实际解决对一段视频进行粗略理解得paper了。个人认为它能够真正能解决视频理解是因为它是对一个模型整体做了训练,而不仅仅是通过visual encoders(e.g BLIP/CLIP/…)和 其它multi modal 的encoder直接过了个projection,做一个…...

Opencv人机交互界面设置

Opencv人机交互界面设置 以下是一些常见的OpenCV人机交互界面设置&#xff1a; 窗口交互 显示窗口&#xff1a;可以使用cv2.imshow()函数在屏幕上显示图像。例如&#xff0c;要显示名为“image”的图像&#xff0c;可以使用以下代码&#xff1a; import cv2img cv2.imread…...

蓝桥杯算法心得——字典树考试(贡献度+前缀和)

大家好&#xff0c;我是晴天学长&#xff0c;贡献度的题&#xff0c;找到技巧非常重要&#xff0c;需要的小伙伴可以关注支持一下哦&#xff01;后续会继续更新的。&#x1f4aa;&#x1f4aa;&#x1f4aa; 1) .字典树考试 字典树考试 问题描述 蓝桥学院最近教学了字典树这一数…...

Linux下Qt生成程序崩溃文件

文章目录 1.背景2.Qt编译生成程序2.1.profile模式的本质 3.执行程序&#xff0c;得到core文件4.代码定位4.1.直接使用gdb4.2.使用QtCreator 5.总结6.题外话6.1.profile模式和debug模式的区别 1.背景 在使用Qt时&#xff0c;假如在windows&#xff0c;当软件崩溃时&#xff0c;…...

Go语言中测试和性能

1. 测试:软件开发最重要的方面 测试软件程序可能是软件开发人员能够做的最重要的事情。通过测试代码的功能,开发人员能够在很大程度上确定程序是有效的。另外,每次修改代码后,开发人员都可运行测试,确认没有引入Bug和衰退。通过测试软件,还能够让软件工程师确认程序按期望…...

SciencePlots——绘制论文中的图片

文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了&#xff1a;一行…...

CMake基础:构建流程详解

目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

Leetcode 3577. Count the Number of Computer Unlocking Permutations

Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接&#xff1a;3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯&#xff0c;要想要能够将所有的电脑解锁&#x…...

在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module

1、为什么要修改 CONNECT 报文&#xff1f; 多租户隔离&#xff1a;自动为接入设备追加租户前缀&#xff0c;后端按 ClientID 拆分队列。零代码鉴权&#xff1a;将入站用户名替换为 OAuth Access-Token&#xff0c;后端 Broker 统一校验。灰度发布&#xff1a;根据 IP/地理位写…...

cf2117E

原题链接&#xff1a;https://codeforces.com/contest/2117/problem/E 题目背景&#xff1a; 给定两个数组a,b&#xff0c;可以执行多次以下操作&#xff1a;选择 i (1 < i < n - 1)&#xff0c;并设置 或&#xff0c;也可以在执行上述操作前执行一次删除任意 和 。求…...

【JavaSE】绘图与事件入门学习笔记

-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角&#xff0c;以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向&#xff0c;距离坐标原点x个像素;第二个是y坐标&#xff0c;表示当前位置为垂直方向&#xff0c;距离坐标原点y个像素。 坐标体系-像素 …...

在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用

1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...

九天毕昇深度学习平台 | 如何安装库?

pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子&#xff1a; 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

用机器学习破解新能源领域的“弃风”难题

音乐发烧友深有体会&#xff0c;玩音乐的本质就是玩电网。火电声音偏暖&#xff0c;水电偏冷&#xff0c;风电偏空旷。至于太阳能发的电&#xff0c;则略显朦胧和单薄。 不知你是否有感觉&#xff0c;近两年家里的音响声音越来越冷&#xff0c;听起来越来越单薄&#xff1f; —…...

让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比

在机器学习的回归分析中&#xff0c;损失函数的选择对模型性能具有决定性影响。均方误差&#xff08;MSE&#xff09;作为经典的损失函数&#xff0c;在处理干净数据时表现优异&#xff0c;但在面对包含异常值的噪声数据时&#xff0c;其对大误差的二次惩罚机制往往导致模型参数…...