【简介】
- 该游戏主要的功能是分为player玩家拿手枪 对野怪进行射击 player ,野怪也可以对玩家进行攻击,野怪可以对玩家进行攻击。野怪可以巡逻 在一定的范围内 可以自动监控到玩家进行攻击。攻击野怪一定的血量后 野怪会被杀死 这时候可以加血。。。
将项目可拆解成以下几个模块:
1.添加场景以及资源导入
2.脚本动态拼接地板
3.player制作
4.角色控制器CharacterController
5.枪后坐力
6.枪口与相机位置的调整
7.子弹连发以及开火音效
8.AI 导航网格和导航代理
9.AI 检测敌人自动开火
10.人物血条UI 显示
11.敌人随机生成
-
添加场景以及资源导入
Assest
目录下为所有资源素材,包含了Player
和Enmey
以及天空盒子 地板的所有素材。
脚本动态拼接地板
动态创建地板以及墙面,可以通过脚本去for 循环遍历依次添加每个地步的位置以及高度
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class Ryunm_FloorLayout : MonoBehaviour
{
public GameObject basicFloor;
public Vector3 cellSet = Vector3.one;
public Vector3 floorSize = Vector3.zero;
public Vector3 StartPos = Vector3.zero;
public List floorList;
[ContextMenu("LayoutFloor")]
public void LayoutFloors()
{
floorList = this.GetComponentsInChildren().ToList();//
for (int i=1;i
然后动态去配置参数 控制地板的数量。
- player制作
Player 玩家主要包括 body身体
Eyes视角
checkPoint瞄准视角
Eyes 主要是由枪的模型
以及相机
视角组成
- 角色控制器CharacterController
player
玩家的移动以及WeaponCamera
的移动可以结合CharacterController的API去操作会更加的方便
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Ryunm_FPS_PlayerController : MonoBehaviour
{
public float mouseOffset_x;
public float mouseOffset_y;
public float h_InputValue;
public float v_InputValue;
public float rotateSpeed = 180;
public Transform playerTransform;
public Transform eyesTransform;
public float x_rotateLimit = 65;
public float x_angleValue = 0;
public CharacterController _player_CC;
public float moveSpeed = 5;
public Vector3 motionVlaue = Vector3.zero;
public float GravityValue = -10;
public float Volecity_y=0;
public bool isFloor = false;
public Transform checkPoint;
public float checkRaius = 0.5f;
public LayerMask floorLayer;
public float jumpHight = 10;
public Ryunm_AnimatorController playerAni;
void Start()
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
if (_player_CC == null)
{
_player_CC = this.GetComponent();
}
isFloor = false;
}
void Update()
{
mouseOffset_x = Input.GetAxis("Mouse X");
mouseOffset_y = Input.GetAxis("Mouse Y");
h_InputValue = Input.GetAxisRaw("Horizontal");
v_InputValue = Input.GetAxisRaw("Vertical");
PlayerViewChange();
PlayerMove();
if (h_InputValue != 0 || v_InputValue != 0)
{
playerAni.Alerted = true;
playerAni.MoveSpeed = moveSpeed;
}
else
{
playerAni.Alerted = false;
playerAni.MoveSpeed = 0;
}
}
void PlayerViewChange()
{
playerTransform.Rotate(Vector3.up*rotateSpeed*Time.deltaTime* mouseOffset_x);
x_angleValue -= rotateSpeed * Time.deltaTime * mouseOffset_y;
x_angleValue = Mathf.Clamp(x_angleValue,-x_rotateLimit, x_rotateLimit);
Quaternion qua = Quaternion.Euler(new Vector3(x_angleValue, eyesTransform.localEulerAngles.y, eyesTransform.localEulerAngles.z)) ;
eyesTransform.localRotation = qua;
}
void PlayerMove()
{
if (_player_CC == null) return;
motionVlaue = Vector3.zero;
float forwardMoveVlaue = v_InputValue * moveSpeed * Time.deltaTime;
motionVlaue += _player_CC.transform.forward * forwardMoveVlaue;
motionVlaue += _player_CC.transform.right * (h_InputValue*moveSpeed*Time.deltaTime);
Volecity_y += GravityValue * Time.deltaTime;
motionVlaue += Vector3.up * Volecity_y * Time.deltaTime * 0.5f;
isFloor = Physics.CheckSphere(checkPoint.position, checkRaius, floorLayer);
if (isFloor)
{
if (Volecity_y < 0)
{
Volecity_y = 0;
}
}
if (Input.GetButtonDown("Jump"))
{
float jumpTime = Mathf.Sqrt(Mathf.Abs(jumpHight * 2 / GravityValue));
Volecity_y = -GravityValue * jumpTime;
print(Volecity_y);
isFloor = false;
}
_player_CC.Move(motionVlaue);
}
}
上面是挂载在Player
身上的脚本 通过获取鼠标的偏移值mouseOffset_x
mouseOffset_y
h_InputValue
v_InputValue
_player_CC 去控制移动 跳。。。
-
枪后坐力以及开火音效
枪在开火的时候会往后偏移 开完火会移到原位置。主要的思路是,定义好前后挪动的位置,在开火 结束开火偏移到对应位置。对应脚本的逻辑如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Ryunm_WeaponController : MonoBehaviour
{
public Transform bulletStartPoint;
public GameObject bulletPrefabs;
public float bulletSpeed = 500;
public AudioSource fireAudio;
public float intervalTime = 0.2f;
public float fireTimer = 0;
public Transform weaponBody;
public Transform startPoint;
public Transform backPoint;
public float lerpValue = 0.2f;
public Camera mainCamera;
public Camera weaponCamera;
public float startFieldView = 60;
public float endFieldView = 30;
public Vector3 weaponCameraStartPoint;
public Vector3 weaponCameraCenterPoint;
public float smoothTime = 0.5f;
public Ryunm_AnimatorController playerAni;
public Slider bulletCountSlider;
public float maxBulletCount = 50;
public float currentBulletCount = 0;
public float reloadSpeed = 5;
public float reloadWaitTime = 2;
void Start()
{
currentBulletCount = maxBulletCount;//Bullet step1
bulletCountSlider = Ryunm_GameManager._instance.bulletCountSlider;
bulletCountSlider.maxValue = maxBulletCount;
bulletCountSlider.value = currentBulletCount;
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
StopCoroutine("RealoadBullet");
OpenFire();
}
if (Input.GetMouseButton(0))
{
fireTimer += Time.deltaTime;
if (fireTimer>= intervalTime)
{
OpenFire();
}
}
if (Input.GetMouseButtonUp(0))
{
//StartCoroutine(RealoadBullet());
StartCoroutine("RealoadBullet");
}
if (Input.GetMouseButtonDown(1))
{
StopCoroutine("ToStart");
StartCoroutine("ToCenter");
}
if (Input.GetMouseButtonUp(1))
{
StopCoroutine("ToCenter");
StartCoroutine("ToStart");
}
}
public void OpenFire()
{
if (currentBulletCount > 0)
{
playerAni.Attack();
fireTimer = 0;
if (fireAudio)
{
fireAudio.Play();
}
GameObject newBullet = Instantiate(bulletPrefabs, bulletStartPoint.position, bulletStartPoint.rotation);
newBullet.GetComponent().bulletType = BulletType.Player_Bullet;
newBullet.GetComponent().velocity = newBullet.transform.forward * bulletSpeed;
currentBulletCount -= 1;
bulletCountSlider.value = currentBulletCount;
StopCoroutine("WeaponBack");
StartCoroutine("WeaponBack");
}
else
{
}
}
IEnumerator WeaponBack()
{
while (weaponBody.localPosition.z>backPoint.localPosition.z+0.005f)
{
weaponBody.localPosition = Vector3.Lerp(weaponBody.localPosition, backPoint.localPosition, lerpValue);
yield return null;
}
while (weaponBody.localPosition.z
-
枪口与相机位置的调整
在player
移动的过程中 相机是跟随的,MainCamera
是在player
的子节点下面这样会导致 相机的视角中也会看到player
,而我们需要的是视野中只有抢的模型。所有我们需要把相机
拆成两个部分 一个视角是正前方 一个视角只能看到WeaponCamera
对应的camera 视角各司其职! 子弹连发以及开火音效
子弹的逻辑就是按下鼠标的时候 就执行openFire方法
void Update()
{
if (Input.GetMouseButtonDown(0))
{
StopCoroutine("RealoadBullet");
OpenFire();
}
if (Input.GetMouseButton(0))
{
fireTimer += Time.deltaTime;
if (fireTimer>= intervalTime)
{
OpenFire();
}
}
if (Input.GetMouseButtonUp(0))
{
//StartCoroutine(RealoadBullet());
StartCoroutine("RealoadBullet");
}
if (Input.GetMouseButtonDown(1))
{
StopCoroutine("ToStart");
StartCoroutine("ToCenter");
}
if (Input.GetMouseButtonUp(1))
{
StopCoroutine("ToCenter");
StartCoroutine("ToStart");
}
WeaponRay();
}
public void OpenFire()
{
if (currentBulletCount > 0)
{
playerAni.Attack();
fireTimer = 0;
if (fireAudio)
{
fireAudio.Play();
}
GameObject newBullet = Instantiate(bulletPrefabs, bulletStartPoint.position, bulletStartPoint.rotation);
newBullet.GetComponent().bulletType = BulletType.Player_Bullet;
newBullet.GetComponent().velocity = newBullet.transform.forward * bulletSpeed;
currentBulletCount -= 1;
bulletCountSlider.value = currentBulletCount;
StopCoroutine("WeaponBack");
StartCoroutine("WeaponBack");
}
else
{
}
}
在openFire方法中 会创建bulletPrefabs
并且朝向坐标为bulletStartPoint
的位置。并且开火时会执行 开火音效
playerAni.Attack();
- AI 导航网格和导航代理
可以借助Unity 提供的NavMeshAgent
去执行导航的逻辑
如上面截图的 在inspect 中勾选 需要参与导航的路径 然后在
Enmey
中挂载NavMeshAgent 控件 ,在脚本添加如下方法 就可以控制其移动逻辑。
- AI 检测敌人自动开火
可以借助上面的逻辑基础上,规划好Enmey
需要巡逻的路径。
WayPoinList 为Enmey
需要规划的位置。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.AI;
public class Ryunm_Enemy : MonoBehaviour
{
public float PH = 100;
public Slider ph_Slider;
public GameObject enemyExplossion;
public NavMeshAgent _EnmeyAgent;
public Transform wayPointParent;
public Transform[] points;
private int destPoint = 0;
public float minDistance = 10;
public float minIntervalTime = 1;
public float maxIntervalTime = 3;
public float minDamge = 5;
public float maxDamge = 15;
public Transform bulletStartPoint;
public GameObject bulletPrefab;
public float bulletSpeed = 200;
public bool isFireOpen = false;
public Ryunm_AnimatorController ani;
public GameObject healthPrefab;
// Start is called before the first frame update
void Start()
{
ani = this.GetComponent();
ph_Slider.maxValue = PH;
ph_Slider.value = PH;
_EnmeyAgent = this.GetComponent();
ani.MoveSpeed = _EnmeyAgent.speed;
if (_EnmeyAgent)
{
_EnmeyAgent.autoBraking = false;//
points = wayPointParent.GetComponentsInChildren();
destPoint = Random.Range(0, points.Length);
}
isFireOpen = false;
}
void Update()
{
if (_EnmeyAgent)
{
//_EnmeyAgent.destination = GameObject.FindObjectOfType().transform.position;
if (!_EnmeyAgent.pathPending && _EnmeyAgent.remainingDistance < 0.5f)
GotoNextPoint();
}
var playerTransform = Ryunm_GameManager._instance._player.transform;
float distance = Vector3.Distance(this.transform.position, playerTransform.position);
if (distance < minDistance)
{
_EnmeyAgent.SetDestination(playerTransform.position);
ani.Alerted = true;
if(isFireOpen == false)
{
StartCoroutine("Fire");
isFireOpen = true;
}
}
else
{
StopCoroutine("Fire");
isFireOpen = false;
ani.Alerted = false;
}
}
void GotoNextPoint()
{
if (points.Length == 0)
return;
_EnmeyAgent.destination = points[destPoint].position;
int temp = Random.Range(0, points.Length);
if (destPoint == temp)
{
destPoint = (destPoint + 1) % points.Length;
}
else
{
destPoint = temp;
}
}
}
上面SetDestination 加入了
if (_EnmeyAgent)
{
//_EnmeyAgent.destination = GameObject.FindObjectOfType().transform.position;
if (!_EnmeyAgent.pathPending && _EnmeyAgent.remainingDistance < 0.5f)
GotoNextPoint();
}
void GotoNextPoint()
{
if (points.Length == 0)
return;
_EnmeyAgent.destination = points[destPoint].position;
int temp = Random.Range(0, points.Length);
if (destPoint == temp)
{
destPoint = (destPoint + 1) % points.Length;
}
else
{
destPoint = temp;
}
}
在阀值区间外 野怪都是会可以任意移动到points 的随机巡逻点坐标下面。在阀值 内 则会去执行开火的逻辑
if (distance < minDistance)
{
_EnmeyAgent.SetDestination(playerTransform.position);
ani.Alerted = true;
if(isFireOpen == false)
{
StartCoroutine("Fire");
isFireOpen = true;
}
}
- 人物血条UI 显示
在2D
界面会用Canvas 绘制。血条Slider
和子弹数量Slider
以及当前当前游戏关卡中最大游戏数量EnmeyCountText
以及剩余数量EnmeyCurrentCountText
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum BulletType
{
Player_Bullet=0,
Enemy_Bullet=1,
}
public class Ryunm_Bullet : MonoBehaviour
{
public BulletType bulletType = BulletType.Player_Bullet;
public float damageValue = 10;
public GameObject bulletExplossion;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
private void OnCollisionEnter(Collision collision)
{
if(bulletType == BulletType.Player_Bullet)
{
if (collision.gameObject.CompareTag("Enemy"))
{
collision.gameObject.GetComponent().OnDamage(damageValue);
}
if (!collision.gameObject.CompareTag("Player"))
{
GameObject newExplossion = Instantiate(bulletExplossion, this.transform.position, bulletExplossion.transform.rotation);
Destroy(this.gameObject);
}
}
if (bulletType == BulletType.Enemy_Bullet)
{
if (collision.gameObject.CompareTag("Player"))
{
collision.gameObject.GetComponent().OnDamage(damageValue);
}
if (!collision.gameObject.CompareTag("Enemy"))
{
GameObject newExplossion = Instantiate(bulletExplossion, this.transform.position, bulletExplossion.transform.rotation);
Destroy(this.gameObject);
}
}
}
private void OnTriggerEnter(Collider other)
{
}
}
在子弹Bullet
上挂载的脚本 会监听发出的子弹和野怪发生碰撞的逻辑
,在碰撞事件OnCollisionEnter
中 会执行Ryunm_Enemy
和Ryunm_PlayerHealth
对应脚本的OnDamage
方法
下面进入Player
下面的Ryunm_PlayerHealth
脚本 可以看到关联了PhSlider
从而关联OnDamage
方法
-
敌人随机生成
GameManager 为一个空物体单例,他关联了 Canvas
的野怪数量
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Ryunm_GameManager : MonoBehaviour
{
public static Ryunm_GameManager _instance;
public Slider bulletCountSlider;
public Ryunm_FPS_PlayerController _player;
public int maxEnemyCount=50;
public int remainingEnemyCount = 0;
public int currentMaxEnemyCount = 10;
public int currentEnemyCount = 0;
public int currentDestoryEnmeyCount = 0;
public Text enemyCountTxt;
public Text currentCountTxt;
public Transform wayPointList;
public Transform[] wayPoints;
public GameObject enmeyPrefab;
private void Awake()
{
_instance = this;
}
// Start is called before the first frame update
void Start()
{
remainingEnemyCount = maxEnemyCount;
currentEnemyCount = 0;
wayPoints = wayPointList.GetComponentsInChildren();
EenmyUIReset();
InvokeRepeating("BornEnmey",0.5f,2);
}
// Update is called once per frame
void Update()
{
}
public void EenmyUIReset()
{
enemyCountTxt.text = currentDestoryEnmeyCount.ToString() + "/" + maxEnemyCount.ToString();
currentCountTxt.text = currentEnemyCount.ToString();
}
private void BornEnmey()
{
if (currentEnemyCount < currentMaxEnemyCount&& remainingEnemyCount>0)
{
int index = Random.Range(0, wayPoints.Length);
GameObject newEnemy = Instantiate(enmeyPrefab, wayPoints[index].position, enmeyPrefab.transform.rotation);
newEnemy.GetComponent().wayPointParent = wayPointList;
remainingEnemyCount -= 1;
currentEnemyCount += 1;
currentCountTxt.text = currentEnemyCount.ToString();
}
}
public void EnmeyBeDestory()
{
currentEnemyCount -= 1;
currentCountTxt.text = currentEnemyCount.ToString();
currentDestoryEnmeyCount += 1;
enemyCountTxt.text = currentDestoryEnmeyCount.ToString() + "/" + maxEnemyCount.ToString();
}
}
脚本定义了 最大的野怪生成数量变量 以及剩余变量 然后InvokeRepeating("BornEnmey",0.5f,2);
去不断生成野怪 ,在满足条件下 一直去生成野怪
if (currentEnemyCount < currentMaxEnemyCount&& remainingEnemyCount>0)
然后对应去现实当前currentCountTxt
和enemyCountTxt
的数量