赞
踩
先将摄像机调整到合适位置。将主⾓的位置参数中的X、Z设置为0,把摄像机置于主⾓后上⽅、斜向下45°即可。摄像机位置参数如图2-39所⽰。
摄像机⽬前还是在固定位置,应改为随主⾓移动⽽平移。将摄像机直接作为主⾓的⼦物体也是可⾏的,但在第2.4.1⼩节已经给出了通⽤的跟随式摄像机实现⽅法,因此在这⾥直接应⽤即可。
将脚本挂载到主摄像机上以后,要注意在编辑器中指定Target对象。指定⽅法是将Hierarchy窗⼝中的Player物体拖曳到脚本组件的Target字段上,如图2-40所⽰。
最后测试游戏,会发现摄像机跟随玩家移动。在商业级游戏开发中,会让摄像机的移动更平滑,⽽这只需要在FollowCam脚本中做⼀些修改即可。
实现玩家的移动不算困难,实现武器系统才是本游戏的特⾊和开发重点。为了充分发挥组件式编程的优点,要把武器系统写成⼀个独⽴的组件Weapon,⽆论主⾓还是敌⼈,都可以调⽤这个武器组件。这会让逻辑的实现更严谨,同时也具有充分的灵活性。
- using UnityEngine;
- public class Weapon : MonoBehaviour
- {
- // ⼦弹的prefab
- public GameObject prefabBullet;
- // ⼏种武器的CD时间⻓度
- public float pistolFireCD = 0.2f;
- public float shotgunFireCD = 0.5f;
- public float rifleCD = 0.1f;
- // 上次开⽕时间
- float lastFireTime;
- // 当前使⽤哪种武器
- public int curGun { get; private set; } // 0 ⼿枪,1 霰
- 弹枪,2 ⾃动步枪
- // 开⽕函数,由⾓⾊脚本调⽤
- // keyDown代表这⼀帧按下开⽕键,keyPressed 代表开⽕键正在持续按下
- // 这样区分是为了实现⼿枪和⾃动步枪的不同⼿感
- public void Fire(bool keyDown, bool keyPressed)
- {
- // 根据当前武器,调⽤对应的开⽕函数
- switch (curGun)
- {
- case 0:
- if (keyDown)
- {
- pistolFire();
- }
- break;
- case 1:
- if (keyDown){
- shotgunFire();
- }
- break;
- case 2:
- if (keyPressed)
- {
- rifleFire();
- }
- break;
- }
- }
- // 更换武器
- public int Change()
- {
- curGun += 1;
- if (curGun == 3)
- {
- curGun = 0;
- }
- return curGun;
- }
- // ⼿枪射击专⽤函数
- public void PistolFire()
- {
- if (lastFireTime + pistolFireCD > Time.time)
- {
- return;
- }
- lastFireTime = Time.time;
- GameObject bullet = Instantiate(prefabBullet, null);
- bullet.transform.position = transform.position +
- transform.forward * 1.0f;
- bullet.transform.forward = transform.forward;
- }
- // ⾃动步枪射击专⽤函数
- public void RifleFire()
- {
- if (lastFireTime + rifleCD > Time.time)
- {
- return;
- }
- lastFireTime = Time.time;
- GameObject bullet = Instantiate(prefabBullet, null);
- bullet.transform.position = transform.position +transform.forward * 1.0f;
- bullet.transform.forward = transform.forward;
- }
- // 霰弹枪射击专⽤函数
- public void shotgunFire()
- {
- if (lastFireTime + shotgunFireCD > Time.time)
- {
- return;
- }
- lastFireTime = Time.time;
- // 创建5颗⼦弹,相互间隔10°,分布于前⽅扇形区域
- for (int i=-2; i<=2; i++)
- {
- GameObject bullet = Instantiate(prefabBullet,
- null);
- Vector3 dir = Quaternion.Euler(0, i * 10, 0) *
- transform.forward;
- bullet.transform.position = transform.position +
- dir * 1.0f;
- bullet.transform.forward = dir;
- // 霰弹枪的⼦弹射击距离很短,因此修改⼦弹的⽣命周期
- Bullet b = bullet.GetComponent<Bullet>();
- b.lifeTime = 0.3f;
- }
- }
- }
上⾯直接给出了完整的Weapon代码,其中的霰弹枪射击的代码可能会让初学者很费解,读者可以暂时注释掉ShotgunFire()函数的后半部分。武器实际上是通过不断创建⼦弹的prefab来发射⼦弹的,因此还需要创建⼦弹的预制体,如图2-41所⽰,其步骤如下。
01 创建⼀个球体,⼤⼩缩放为0.3倍,并增添⾦⻩⾊材质。
02 添加Rigidbody组件,勾选Is Kinematic选项。
03 勾选Sphere Collider组件中的Is Trigger选项。
04 新建Bullet脚本,其内容如下,并将其挂载于⼦弹上。
05 将⼦弹拖曳到Project窗⼝中,做成prefab。
- using UnityEngine;
- public class Bullet : MonoBehaviour
- {
- // ⼦弹⻜⾏速度
- public float speed = 10.0f;
- // ⼦弹⽣命期(⼏秒之后消失)
- public float lifeTime = 2;
- // ⼦弹“出⽣”的时间
- float startTime;
- void Start()
- {
- startTime = Time.time;
- }
- void Update()
- {
- // ⼦弹移动
- transform.position += speed * transform.forward *
- Time.deltaTime;
- // 超过⼀定时间销毁⾃⾝
- if (startTime + lifeTime < Time.time)
- {
- Destroy(gameObject);
- }
- }
- // 当⼦弹碰到其他物体时触发
- private void OnTriggerEnter(Collider other)
- {
- // 稍后补充碰撞的逻辑
- }
接下来将武器逻辑与玩家逻辑联系起来,确保将Weapon脚本挂载到玩家物体上,将Bullet脚本挂载到⼦弹物体上,将Bullet的预制体拖曳到Weapon脚本的prefabBullet字段上。
如果这时测试,会发现缺少按键开⽕的逻辑,因此还需要修改Player脚本。⾸先,给Player脚本添加Weapon组件的引⽤,并在Start()函数中初始化,⽅便稍后使⽤武器。
- // 以下是代码⽚段,添加于Player脚本中
- Weapon weapon;
- void Start()
- {
- hp = maxHp;
- weapon = GetComponent<Weapon>();
- }
Update()函数修改较多,因为要增加按键逻辑。其中J键⽤于开⽕,Q键⽤于切换3种武器。修改后的Update()函数如下:
- void Update()
- {
- input = new Vector3(Input.GetAxis("Horizontal"), 0,
- Input.GetAxis("Vertical"));
- Debug.Log(input);
- bool fireKeyDown = Input.GetKeyDown(KeyCode.J);
- bool fireKeyPressed = Input.GetKey(KeyCode.J);
- bool changeWeapon = Input.GetKeyDown(KeyCode.Q);
- if (!dead)
- {
- Move();
- weapon.Fire(fireKeyDown, fireKeyPressed);
- if (changeWeapon)
- {
- ChangeWeapon();
- }
- }
- }
- 新增的更换武器的ChangeWeapon()函数内容如下。private void ChangeWeapon()
- {
- int w = weapon.Change();
- }
⾄此,武器逻辑已经基本完成,可以运⾏游戏进⾏测试,重点测试主⾓的移动、射击等功能,如图2-42所⽰。射击与切换武器的键分别是J和Q。也可以考虑加快主⾓的移动速度和⼦弹⻜⾏速度。
由于敌⼈要随时找到并攻击玩家⾓⾊,因此敌⼈需要玩家⾓⾊的引⽤。先将玩家物体的标签设置为Player,然后创建敌⼈⾓⾊。敌⼈⾓⾊与玩家⾓⾊造型基本⼀致,只是将⽩⾊换成深蓝⾊,如图2-43所⽰。当然读者也可以⾃⼰发挥,做出更有趣的外形。
- 为敌⼈⾓⾊编写脚本Enemy,其内容如下。
- using UnityEngine;
- public class Enemy : MonoBehaviour
- {
- // ⽤于制作死亡效果的预制体,暂时不⽤
- public GameObject prefabBoomEffect;
- public float speed = 2;
- public float fireTime = 0.1f;
- public float maxHp = 1;
- Vector3 input;
- Transform player;
- float hp;
- bool dead = false;
- Weapon weapon;void Start()
- {
- // 根据Tag查找玩家物体
- player =
- GameObject.FindGameObjectWithTag("Player").transform;
- weapon = GetComponent<Weapon>();
- }
- void Update()
- {
- Move();
- Fire();
- }
- void Move()
- {
- // 玩家的input是从键盘输⼊⽽来,⽽敌⼈的input则是始终指向玩家的
- ⽅向
- input = player.position - transform.position;
- input = input.normalized;
- transform.position += input * speed * Time.deltaTime;
- if (input.magnitude > 0.1f)
- {
- transform.forward = input;
- }
- }
- void Fire()
- {
- // ⼀直开枪,开枪的频率可以通过武器启动控制
- weapon.Fire(true, true);
- }
- private void OnTriggerEnter(Collider other)
- {
- // 稍后实现
- }
- }
可以看出,敌⼈⾓⾊的代码完全可以仿照玩家⾓⾊编写,且随着对脚本的逐渐熟悉,某些复杂的功能的代码,如AI,会显得越来越简单。以上代码还复⽤了之前写好的Weapon组件,可⻅之前对武器系统的组件化是很有⽤的。
敌⼈⾓⾊脚本编写完成后,将Enemy脚本和Weapon脚本都挂载到敌⼈⾓⾊⾝上,那么敌⼈⾓⾊的实现就完成了。可以将敌⼈⾓⾊制作成预制体,⽅便之后创建多个敌⼈以进⾏测试。接下来创建⼀个敌⼈⾓⾊进⾏测试,会发现敌⼈⾓⾊正在追击主⾓并不断开⽕,如图2-44所⽰。 最后,可以在编辑器中调整Player脚本组件中的主⾓移动速度、Weapon组件中的武器冷却时间,以及Enemy脚本组件中的敌⼈移动速度。由于Weapon脚本已经分别挂载到了玩家和敌⼈⾝上,因此玩家⾓⾊和每个敌⼈⾓⾊的武器发射频率都可以单独调整,互不影响。
碰撞事件是相对的,因此⼦弹碰撞的逻辑可以写在Bullet脚本、Player脚本或Enemy脚本中。在制作时最好单独考虑碰撞问题,然后统⼀编写。要制作的效果如下。
①玩家⾓⾊的⼦弹击中敌⼈⾓⾊,会让敌⼈⾓⾊掉⾎。
②敌⼈⾓⾊的⼦弹击中玩家⾓⾊,会让玩家⾓⾊掉⾎。
③玩家⾓⾊的⼦弹不会击中玩家⾓⾊,敌⼈⾓⾊的⼦弹也不会击中敌⼈⾓⾊。
④玩家⾓⾊的⼦弹与敌⼈⾓⾊的⼦弹可以相互抵消,但是同类⼦弹不能抵消。这⼀设计可以根据读者的偏好添加或取消。从以上分析可以看出,“敌⼈⾓⾊的⼦弹”与“玩家⾓⾊的⼦弹”是截然不同的,最好⽤某种机制区分出⼆者。这⾥编者采⽤的⽅法是把这两种⼦弹做成两个不同的预制体,⼀个命名为PlayerBullet,另⼀个命名为EnemyBullet。然后利⽤标签(Tag)做出区分,前者的Tag是PlayerBullet,后者的Tag是EnemyBullet。具体操作是,⾸先复制之前做好的⼦弹的预制体,然后分别命名为PlayerBullet和EnemyBullet,最后双击PlayerBullet,将其Tag设为PlayerBullet,并为prefab选中标签,如图2-45所⽰。同理给EnemyBullet设置Tag为EnemyBullet。
做好两种⼦弹以后,注意要给玩家⾓⾊⾝上的Weapon组件和敌⼈⾓⾊⾝上的Weapon组件分别设置新的⼦弹预制体,这样就可以让两种⼦弹既使⽤同样的脚本,⼜有了明确区分。接着添加脚本碰撞逻辑,修改Bullet脚本,添加如下触发逻辑。
- // 当⼦弹碰到其他物体时触发
- private void OnTriggerEnter(Collider other)
- {
- // 如果⼦弹的Tag与被碰撞的物体的Tag相同,则不算打中
- // 这是为了防⽌同类⼦弹互相碰撞抵消
- if (CompareTag(other.tag))
- {
- return;
- }
- Destroy(gameObject);
- }
以上代码的作⽤是让⼦弹碰到任何其他物体后消失,包括碰到对⽅⼦弹也会消失。
- 接下来添加玩家被⼦弹击中的逻辑,修改Player脚本的
- OnTriggerEnter函数如下。
- private void OnTriggerEnter(Collider other)
- {
- if (other.CompareTag("EnemyBullet"))
- {
- if (hp <= 0) { return; }
- hp--;
- if (hp <= 0) { dead = true; }
- }
- }
- 最后是敌⼈被击中的逻辑,修改Enemy脚本的OnTriggerEnter函数如
- 下。
- private void OnTriggerEnter(Collider other)
- {
- if (other.CompareTag("PlayerBullet"))
- {
- Destroy(other.gameObject);
- hp--;
- if (hp <= 0)
- {
- dead = true;
- // 这⾥可以添加死亡特效
- //Instantiate(prefabBoomEffect,
- transform.position, transform.rotation);
- Destroy(gameObject);
- }
- }
- }
通过以上修改,可以发现敌⼈的⼦弹已经能够击中玩家,且击中10次以后玩家会死亡,死亡的表现是不能再移动或射击了。如果有⼦弹⽆法碰撞的问题,则需要检查和确认。
01 敌⼈和玩家的⼦弹都必须有碰撞体和刚体,在Rigidbody组件中已经勾选了Is Kinematic选项,且碰撞体组件中已经勾选了Is Trigger选项。
02与触发相关的函数已正确编写,⼦弹Tag已经正确设置。
03 如果读者设置的⼦弹速度很快,建议将⼦弹Rigidbody组件的Collision Detection选项设置为Continuous Dynamic(连续动态检测),如图2-46所⽰。这个选项可以确保⾼速⻜⾏的物体不会错过碰撞事件。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。