《燃烧的地平线》是一款类坦克世界/战争雷霆的第三人称载具射击游戏,是我大三学期的游戏开发基础课程期末作品,该作品使用Unity3D引擎开发。本博客旨记录开发心得,分享开发经验,以及纪念这款虽然很拉跨但是是自己独立开发的游戏。由于本人尚在学习阶段,开发手法也不是很标准,欢迎大佬们指出错误和不足。

最后需要说明的是本项目无任何商业用途。所使用模型及粒子特效来自互联网。其中模型及粒子特效采用了POLYGON的二战模型包。其余部分均由本人独立开发完成。


游戏项目展示:《燃烧的地平线》——Unity3D游戏开发课期末作品展示
源代码:Burning-Horizon-Source-Code


要讲解这个项目,先要说明我做了些什么。这是该项目大概的几个模块。

  • Player模块
  • Enemy模块
  • 炮弹模块
  • 损毁模块
  • Scene模块

Player模块主要负责处理玩家的输入并实现玩家坦克的移动,瞄准,开火,玩家坦克的各项状态,玩家坦克的音效播放以及摄像机控制。
Enemy模块主要负责处理敌人的索敌,瞄准,开火,移动,敌人坦克的各项状态以及敌人坦克的音效播放。
炮弹模块负责处理炮弹的碰撞检测,以及火力测试,炮弹添加了RigidBody以及Trail Renderer
损毁模块主要负责坦克的销毁功能。
Scene模块主要负责场景里的UI管理,关卡管理,游戏的暂停和退出。

接下来我会分开讲一讲各个模块的具体实现


Player模块

"Player模块主要负责处理玩家的输入并实现玩家坦克的移动,瞄准,开火,玩家坦克的各项状态,玩家坦克的音效播放以及摄像机控制。"

其中我最先实现的就是坦克的移动控制。在我的设想中,不求有多么精细的履带及悬挂效果,只需要玩家操控的坦克是个刚体,并且移动时履带和轮子看得到效果就可以。
以下是TankController脚本,该脚本实现了以上效果。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class TankController : MonoBehaviour
{public Rigidbody rb;//坦克左边的所有轮子public GameObject[] LeftWheels;//坦克右边的所有轮子public GameObject[] RightWheels;//坦克左边的履带public GameObject LeftTrack;//坦克右边的履带public GameObject RightTrack;public float wheelSpeed = 2f;public float trackSpeed = 2f;public float rotateSpeed = 10f;public float moveSpeed = 2f;public AudioSource movementAudioPlayer;public AudioClip move;public AudioClip idle;private void Update(){// 获取输入float horizontal = Input.GetAxis("Horizontal");float vertical = Input.GetAxis("Vertical");// 音效播放if (horizontal == 0 && vertical == 0){movementAudioPlayer.clip = idle;if (!movementAudioPlayer.isPlaying){movementAudioPlayer.volume = 0.2f;movementAudioPlayer.Play();}}else{movementAudioPlayer.clip = move;if(!movementAudioPlayer.isPlaying){movementAudioPlayer.volume = 0.6f;movementAudioPlayer.Play();}}// 限制倒车的速度vertical = Mathf.Clamp(vertical, -0.3f, 1f);// 这些都是为了让履带和轮子看上去在动//坦克左右两边车轮转动foreach (var wheel in LeftWheels){wheel.transform.Rotate(new Vector3(wheelSpeed * vertical, 0f, 0f));wheel.transform.Rotate(new Vector3(wheelSpeed * 0.6f * horizontal, 0f, 0f));}foreach (var wheel in RightWheels){wheel.transform.Rotate(new Vector3(wheelSpeed * vertical, 0f, 0f));wheel.transform.Rotate(new Vector3(wheelSpeed * 0.6f * - horizontal, 0f, 0f));}//履带滚动效果// 前后LeftTrack.transform.GetComponent<MeshRenderer>().material.mainTextureOffset += new Vector2(0, -trackSpeed * vertical * Time.deltaTime);RightTrack.transform.GetComponent<MeshRenderer>().material.mainTextureOffset += new Vector2(0, -trackSpeed * vertical * Time.deltaTime);// 左右LeftTrack.transform.GetComponent<MeshRenderer>().material.mainTextureOffset += new Vector2(0, 0.6f * -trackSpeed * horizontal * Time.deltaTime);RightTrack.transform.GetComponent<MeshRenderer>().material.mainTextureOffset += new Vector2(0, 0.6f * trackSpeed * horizontal * Time.deltaTime);// 坦克本体的移动rb.MovePosition(rb.position + transform.forward * moveSpeed * vertical * Time.deltaTime);// 坦克本体的旋转Quaternion turnRotation = Quaternion.Euler(0f, horizontal * rotateSpeed * Time.deltaTime, 0f);rb.MoveRotation(rb.rotation * turnRotation);}
}

原谅我并未做任何封装,一是因为项目本身不大,二是因为编写时从未进行过策划。
这个脚本的设计思路是,分别拿到坦克左右两边的所有车轮以及两条履带的材质,材质一定要是两个不同的材质,履带是不动的,动起来的是履带上附带的材质,这样也可以实现视觉上的履带移动效果。车轮则会实际旋转起来。具体运动方向根据玩家的输入来控制坦克的车轮旋转和履带材质的位移以符合坦克的移动方式,是前进后退还是左转右转,或者更复杂的前进后退的同时在旋转。
为什么要这样做?因为坦克和汽车的移动是不同的,坦克靠两条履带的运动的来前进,靠两边的速度差来旋转。所以我们的效果要符合。
视觉上是这样的:《燃烧的地平线》履带和车轮的运动效果

坦克的瞄准算是这个项目的难点了,参考了一些大佬的代码和项目。最终效果勉强及格。TankAimming脚本代码如下。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class TankAimming : MonoBehaviour
{// 旋转速度public float rotateSpeed;// 炮塔的Transformpublic Transform turret;// 炮管的Transformpublic Transform gun;// 火炮瞄准UI图片public Image GunAimImage;// 炮口的Transformpublic Transform gunPoint;// 炮管的仰角[Range(0.0f, 90.0f)]public float elevation = 25f;// 炮管的俯角[Range(0.0f, 90.0f)]public float depression = 10f;// 当前正在使用的摄像机public Camera currentCamera;// 炮塔锁死功能private bool isLocked = false;private void Start(){Cursor.lockState = CursorLockMode.Locked;Cursor.visible = false;}void Update(){// 用于存储瞄准的方向Vector3 aimPosition/* = Camera.main.transform.TransformPoint(Vector3.forward * 10000.0f)*/;// 用于确定射线的落点RaycastHit camHit;// 射线的最大距离float maxDistance = 10000f;// 用于Debug.DrawRayfloat camDistance = 0f;// 从当前使用的摄像机位置向前发射射线if (Physics.Raycast(currentCamera.transform.position,currentCamera.transform.forward,out camHit, maxDistance, LayerMask.GetMask("Default", "Ground", "Enemy"))){aimPosition = camHit.point;camDistance = camHit.distance;}else{aimPosition = currentCamera.transform.TransformDirection(Vector3.forward) * maxDistance;camDistance = maxDistance;}// 右键锁死坦克炮塔if (Input.GetMouseButton(1)){isLocked = true;}else{isLocked = false;}// 如果炮塔没有锁死if (!isLocked){ // 炮塔的实际旋转Vector3 turretPos = transform.InverseTransformPoint(aimPosition);turretPos.y = 0f;  //过滤掉y轴的信息,防止炮塔出现绕x,z轴旋转的问题Quaternion aimRotTurret = Quaternion.RotateTowards(turret.localRotation,Quaternion.LookRotation(turretPos), Time.deltaTime * rotateSpeed);turret.localRotation = aimRotTurret;// 炮管的实际旋转Vector3 localTargetPos = turret.InverseTransformPoint(aimPosition);localTargetPos.x = 0f; //过滤掉x轴的信息,防止炮塔出现绕y,z轴旋转的问题Vector3 clampedLocalVec2Target = localTargetPos;// 根据俯仰角限制炮管的旋转角度if (localTargetPos.y >= 0.0f)clampedLocalVec2Target = Vector3.RotateTowards(Vector3.forward, localTargetPos, Mathf.Deg2Rad * elevation, float.MaxValue);elseclampedLocalVec2Target = Vector3.RotateTowards(Vector3.forward, localTargetPos, Mathf.Deg2Rad * depression, float.MaxValue);Quaternion aimRotGun = Quaternion.RotateTowards(gun.localRotation, Quaternion.LookRotation(clampedLocalVec2Target), Time.deltaTime * rotateSpeed);gun.localRotation = aimRotGun;}// 炮管的瞄准UIRaycastHit gunHit;Vector3 UIPos;float gunDistance = 100f;if (Physics.Raycast(gunPoint.position,gunPoint.TransformDirection(Vector3.forward),out gunHit, maxDistance,LayerMask.GetMask("Default", "Ground", "Enemy"))){gunDistance = gunHit.distance;UIPos = gunHit.point;}else{gunDistance = 100f;UIPos = gunPoint.position + gunPoint.forward * gunDistance;}GunAimImage.rectTransform.position = currentCamera.WorldToScreenPoint(UIPos);Debug.DrawRay(gunPoint.position, gunPoint.forward * gunDistance, Color.red);Debug.DrawRay(currentCamera.transform.position, currentCamera.transform.TransformDirection(Vector3.forward) * camDistance, Color.blue);}
}

这个脚本的原理,简单来说,从摄像机发射一条射线,射线打到场景中的某个点(或者什么也没达到就默认向前10000个单位)之后我们拿到那个点作为我们将要旋转到的目标点。再将炮塔和炮管分别旋转到他们该旋转到的位置。
这个脚本让我踩了很多坑,感兴趣可以参考一下我以前写的博客和这个视频:
二战美军谢尔曼的真实战斗力(bushi)
这个脚本最核心的就是Quaternion.RotateTowards这个API的使用,在这个情况下要使用localRotation,aimPosition也要转换到本地坐标系。
在这个脚本中我也尝试做了一个炮管指向的UI,类似坦克世界的瞄准环。实现原理就是从炮管发射一条射线,将射线的命中点作为UI的位置,然后将其转换到屏幕坐标系。
效果:瞄准系统
本条更新于2021.6.22
炮塔瞄准旋转的最终方案为:

    // 炮塔Handle的Transform Handle自身永远不旋转public Transform turretHandle;// 炮管Handle的Transformpublic Transform gunHandle;void Update(){// 如果炮塔没有锁死if (!isLocked){ // 炮塔的实际旋转Vector3 localTurretTarget = turretHandle.InverseTransformPoint(aimPosition);//问题出现的原因是因为使用this.transform调用的InverseTransformPointlocalTurretTarget.y = 0f;  //过滤掉y轴的信息,防止炮塔出现绕x,z轴旋转的问题Quaternion aimRotTurret = Quaternion.RotateTowards(turret.localRotation,Quaternion.LookRotation(localTurretTarget), Time.deltaTime * rotateSpeed); turret.localRotation = aimRotTurret;// 炮管的实际旋转Vector3 localGunTargetPos = gunHandle.InverseTransformPoint(aimPosition);localGunTargetPos.x = 0f; //过滤掉x轴的信息,防止炮塔出现绕y,z轴旋转的问题Vector3 clampedLocalVec2Target = localGunTargetPos;// 根据俯仰角限制炮管的旋转角度if (localGunTargetPos.y >= 0.0f)clampedLocalVec2Target = Vector3.RotateTowards(Vector3.forward, localGunTargetPos, Mathf.Deg2Rad * elevation, float.MaxValue);elseclampedLocalVec2Target = Vector3.RotateTowards(Vector3.forward, localGunTargetPos, Mathf.Deg2Rad * depression, float.MaxValue);Quaternion aimRotGun = Quaternion.RotateTowards(gun.localRotation,Quaternion.LookRotation(clampedLocalVec2Target), Time.deltaTime * rotateSpeed);gun.localRotation = aimRotGun;}// ...

之前出现的炮管和要瞄准的点旋转不一致的问题是因为使用了错误的对象this.transform调用InverseTransformPoint,炮塔的旋转就应该以炮塔作为本地坐标原点,炮管同理,同样的,为了避免以旋转的物体作为本地坐标系导致坐标系变动,我们使用不旋转的Handle作为本地坐标系,需要给实际旋转的炮塔和炮管再加一个不旋转的父物体作为Handle。

时隔半年,再修BUG,笑死了

坦克开火,其实很简单,在炮口位置生成炮弹,粒子特效,播放音效即可,但是我们还需要实现炮弹装填,炮弹装填其实说白了就是一个bool状态,如果是否以装填是true,就可以开火。下面是FireShell脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class FireShell : MonoBehaviour
{public GameObject shell;public Transform gunPoint;public float reactionForce = 10f;public float LoadTime = 5f; //装填时间public GameObject fireFX;public AudioSource fireAudioPlayer;private bool isLoaded; //用于标记是否装填完成private bool startLoading; //用于标记是否要进行装填private void Loaded(){isLoaded = true;}private void Start(){isLoaded = true;startLoading = false;}// Update is called once per framevoid Update(){if(isLoaded){if (Input.GetMouseButtonDown(0)){Instantiate(shell, gunPoint.position, gunPoint.rotation);this.GetComponent<Rigidbody>().AddForceAtPosition(-gunPoint.forward * reactionForce, gunPoint.position, ForceMode.Impulse);GameObject fireFXTemp = Instantiate(fireFX, gunPoint.position, gunPoint.rotation);GameObject.Destroy(fireFXTemp, 3f);startLoading = true;isLoaded = false;fireAudioPlayer.Play();}}if(startLoading){Invoke("Loaded", LoadTime);startLoading = false;}}
}

为了实现"装填要花一点时间才能完成”这个目的,在这里我使用Invoke在LoadTime过后再执行Loaded,Loaded将isLoaded设置为true,startLoading用于确保Invoke只会执行一次。当isLoaded为false时,if里面的代码无法执行,也就无法射击,以此实现了装填效果。
值得一提的是,项目并没有使用对象池来管理炮弹对象,考虑到开发成本以及收益。

接下来是玩家坦克的各项状态Player脚本,坦克的各项状态在我的项目中并不多,我只考虑了装甲值(用于火力测试),是否被击毁,是否处于瘫痪状态。其中瘫痪效果就在本脚本中实现。Player脚本的代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour
{public float armor = 3f;public PlayerDestoryed playerDestoryed;// Bailed Out特效public GameObject bailedOutFireFx;public Transform bialedOutFireTrans;public float bailedOutTime = 5f;[HideInInspector]public bool isBailedOut;[HideInInspector]public bool startBailedOut; //专门用来启动Invokepublic Transform turret;private void BailedOutOver(){isBailedOut = false;}private void Start(){playerDestoryed = GameObject.FindGameObjectWithTag("GameManager").GetComponent<PlayerDestoryed>();isBailedOut = false;startBailedOut = false;}private void Update(){if (startBailedOut){GameObject fireTemp = Instantiate(bailedOutFireFx, bialedOutFireTrans.position, Quaternion.identity);GameObject.Destroy(fireTemp, bailedOutTime);Invoke("BailedOutOver", bailedOutTime);startBailedOut = false;}if(isBailedOut){this.GetComponent<TankController>().enabled = false;this.GetComponent<TankAimming>().enabled = false;this.GetComponent<FireShell>().enabled = false;}else{this.GetComponent<TankController>().enabled = true;this.GetComponent<TankAimming>().enabled = true;this.GetComponent<FireShell>().enabled = true;}}
}

所谓坦克瘫痪,就是坦克由于受到攻击而暂时无法行动,可能是乘员昏厥,成员跳车了,或者坦克某个部件暂时损坏,在我的游戏中都被归为一种情况,既坦克暂时无法行动。isBailedOut用于表示这个状态,当isBailedOut为true时,坦克进入瘫痪状态。在瘫痪状态时,与操作相关的三个脚本组件被设置为disabled,以此来实现无法控制的效果。同时会在坦克尾部生成火焰粒子效果来实现视觉效果。
效果展示:坦克瘫痪效果展示

最后我想直接跳过音效讲解我的摄像机控制,这个项目使用了两种摄像机,一个第三人称摄像机,一个第一人称摄像机(但是游戏依旧被我归类为第三人称射击游戏)。这两种摄像机各有用处,其中一人称摄像机设计为类似于炮手锚具那样的效果,可以像狙击枪瞄具一样放大图像,可以用鼠标滚轮来调整放大倍率,第三人称摄像机也能通过滚轮来调整远近。这套系统的关键在于两个不同的摄像机如何配合。在项目中,两种摄像机设计为通过左shift键或滚轮划到底来切换。先展示一下两种摄像机的脚本
第一人称摄像机脚本FPCamera:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class FPCamera : MonoBehaviour
{public Camera camera;public float mouseSentivity;public Vector2 maxMinAngle;public float mouseScrollSpeed = 5f;// 当前是否是第三人称public bool isThirdPerson;// 第三人称摄像机的鼠标灵敏度public float TPCameraMouseSensitivity;// 摄像机转换public CameraShift shifter;// 旋转运动平滑时间public float rotationSmoothTime = 0.12f;private Vector3 m_mouseInputValue;private float mouseScrollWheel;// 当前的旋转Vector3 currentRotation;// 旋转运动平滑速度Vector3 rotationSmoothVelocity;// 实际鼠标灵敏度[HideInInspector]public float actualMouseSensitivity;[HideInInspector]public float fov = 40f;private void Start(){m_mouseInputValue = new Vector3();camera = this.GetComponent<Camera>();fov = 40f;}void Update(){// 判断当前是否正在使用第三人称摄像机if(isThirdPerson){actualMouseSensitivity = TPCameraMouseSensitivity;}else{actualMouseSensitivity = mouseSentivity;// 只有在第一人称时会设置摄像机fovmouseScrollWheel = -Input.GetAxis("Mouse ScrollWheel");actualMouseSensitivity = 0.5f + (fov / 10);fov += mouseScrollWheel * mouseScrollSpeed;fov = Mathf.Clamp(fov, 5f, 45f); //实际到45f时会切换为第三人称视角camera.fieldOfView = Mathf.Clamp(fov, 5f, 40f);}m_mouseInputValue.y += Input.GetAxis("Mouse X") * actualMouseSensitivity;m_mouseInputValue.x -= Input.GetAxis("Mouse Y") * actualMouseSensitivity;// 限制垂直旋转的角度(以符合现实情况)m_mouseInputValue.x = Mathf.Clamp(m_mouseInputValue.x, maxMinAngle.x, maxMinAngle.y);currentRotation = Vector3.SmoothDamp(currentRotation, new Vector3(m_mouseInputValue.x, m_mouseInputValue.y, 0f), ref rotationSmoothVelocity, rotationSmoothTime);}private void LateUpdate(){Vector3 targetRotation = currentRotation;transform.eulerAngles = targetRotation;}
}

狙击枪瞄具效果并不神秘,调整摄像机的FOV值就可以了。一个细节的处理是在调整FOV值的同时脚本也在调整鼠标输入的灵敏度。第一人称摄像机的实现并不稀奇,网上一抓一大把,这里就不讲了。注意限制绕x轴旋转的角度。

第三人称摄像机ThirdPersonCamera:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ThirdPersonCamera : MonoBehaviour
{// 鼠标灵敏度public float mouseSensitivity = 10f;// 滚轮灵敏度public float mouseScrollSensitivity = 5f;// 第三人称目标public Transform target;// 摄像机离目标的位置public float distanceFromTarget = 2f;// y方向的限制public Vector2 pitchMinMax = new Vector2(-40f, 85f);// 旋转运动平滑时间public float rotationSmoothTime = 0.12f;// 是否锁定鼠标public bool lockCursor;// 当前是否是第一人称public bool isFirstPerson;// 第一人称摄像机的鼠标灵敏度public float FPCameraMouseSensitivity;// 摄像机转换public CameraShift shifter;// x方向private float yaw;// y方向private float pitch;// 旋转运动平滑速度Vector3 rotationSmoothVelocity;// 当前的旋转Vector3 currentRotation;// 实际鼠标灵敏度[HideInInspector]public float actualMouseSensitivity = 10f;// 滚轮拉近拉远摄像机[HideInInspector]public float distance;void Start(){if(lockCursor){Cursor.lockState = CursorLockMode.Locked;Cursor.visible = false;}distance = distanceFromTarget;}private void Update(){// 当前是否是第一人称if(isFirstPerson){actualMouseSensitivity = FPCameraMouseSensitivity;}else{actualMouseSensitivity = mouseSensitivity;// 只有在第三人称时才考虑摄像机拉近拉远distance -= Input.GetAxis("Mouse ScrollWheel") * mouseScrollSensitivity;distance = Mathf.Clamp(distance, 2.5f, 16f); // 实际到2.5f时会切换到第一人称视角distanceFromTarget = Mathf.Clamp(distance, 3f, 16f);}yaw += Input.GetAxis("Mouse X") * actualMouseSensitivity;pitch -= Input.GetAxis("Mouse Y") * actualMouseSensitivity;pitch = Mathf.Clamp(pitch, pitchMinMax.x, pitchMinMax.y);currentRotation = Vector3.SmoothDamp(currentRotation, new Vector3(pitch, yaw, 0f), ref rotationSmoothVelocity, rotationSmoothTime);}// 使用LateUpdate在target.position设置好以后设置摄像机的位置void LateUpdate(){Vector3 targetRotation = currentRotation;transform.eulerAngles = targetRotation;transform.position = target.position - transform.forward * distanceFromTarget;}
}

“平平无奇网上一抓一大把“,但是这个脚本学的油管大佬Sebastian Lague的写法。注意将摄像机的位置设置放在LateUpdate中进行。

你可能注意到了在两个脚本中都会判断当前是第几人称,这是因为切换摄像机没有完全disable掉对应的游戏对象,而是将对应游戏对象的Camera和AudioListener组件disable掉,脚本保持运行,这样能确保两台摄像机不是独立的,如果是独立的,第三人称摄像机看的一个方向,切换到第一人称又看向另一个方向,这样的体验很糟糕,两个脚本无论当前是哪个摄像机实际在工作都会保持运行,以确保一直能获取同样的输入,来实现方向同步,方法很朋克,也有bug没解决。迫于时间我采取了我认为的简单方法。我觉得大可以在切换时将一个摄像机的rotation交给了另一个摄像机,或许会简单一点?你可以试试。
CameraShift脚本如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class CameraShift : MonoBehaviour
{public Transform TPCamera;public Transform FPCamera;public TankAimming tankAimming;private bool isThirdPerson = true;private void Start(){tankAimming.currentCamera = TPCamera.GetComponent<Camera>();}void Update(){// 左shift键可以切换摄像机if(Input.GetKeyDown(KeyCode.LeftShift)){isThirdPerson = !isThirdPerson;}//滚轮拉到一定程度也可以if (isThirdPerson && TPCamera.GetComponent<ThirdPersonCamera>().distance <= 2.5f){isThirdPerson = false;TPCamera.GetComponent<ThirdPersonCamera>().distance = 3f;}if (!isThirdPerson && FPCamera.GetComponent<FPCamera>().fov >= 45f){isThirdPerson = true;FPCamera.GetComponent<FPCamera>().fov = 40f;}if (isThirdPerson) //当前使用第三人称摄像机{// 设置第一人称摄像机FPCamera.GetComponent<FPCamera>().isThirdPerson = true;FPCamera.GetComponent<FPCamera>().TPCameraMouseSensitivity = TPCamera.GetComponent<ThirdPersonCamera>().actualMouseSensitivity;FPCamera.GetComponent<Camera>().enabled = false;FPCamera.GetComponent<AudioListener>().enabled = false;// 设置第三人称摄像机TPCamera.GetComponent<ThirdPersonCamera>().isFirstPerson = false;TPCamera.GetComponent<Camera>().enabled = true;TPCamera.GetComponent<AudioListener>().enabled = true;tankAimming.currentCamera = TPCamera.GetComponent<Camera>();}else //当前使用第一人称摄像机{// 设置第一人称摄像机FPCamera.GetComponent<FPCamera>().isThirdPerson = false;FPCamera.GetComponent<Camera>().enabled = true;FPCamera.GetComponent<AudioListener>().enabled = true;// 设置第三人称摄像机TPCamera.GetComponent<ThirdPersonCamera>().isFirstPerson = true;TPCamera.GetComponent<ThirdPersonCamera>().FPCameraMouseSensitivity = FPCamera.GetComponent<FPCamera>().actualMouseSensitivity;TPCamera.GetComponent<Camera>().enabled = false;TPCamera.GetComponent<AudioListener>().enabled = false;tankAimming.currentCamera = FPCamera.GetComponentInChildren<Camera>();}}
}

可以看到我确实只disable掉了Camera和AudioListener组件。对滚轮操作的处理比较直接。


Enemy模块

“Enemy模块主要负责处理敌人的索敌,瞄准,开火,移动,敌人坦克的各项状态以及敌人坦克的音效播放。”
敌人的索敌,瞄准,开火,在我的项目中敌人会一直向玩家所在方向发射射线,如果射线打到的是玩家,也就表明敌人“发现”玩家了,只有在发现玩家的情况下敌人才会将炮塔转向玩家,于此同时会在炮口位置也发射射线,如果射线打到的是玩家,就说明敌人瞄准玩家了,只有在瞄准玩家并且炮弹装填完毕的情况下敌人才会向玩家开火。至于为啥敌人的侦测能力那么强,你可以理解为德军车长都是不怕死的超人,能做到随时探头并且360度无死角侦察。
因为写的比较早,所以和敌人的状态一起写在了Enemy脚本中了,索敌,瞄准,开火的逻辑代码片段如下:

        // 是否能直接看到玩家(射线检测)RaycastHit hit;// 能否看见玩家if(Physics.Raycast(watchTower.position, aimmingPosition.position - watchTower.position, out hit, detectDistance)){if(hit.transform.tag == "Player"){// 能看到玩家canSeePlayer = true;// 是否已经瞄准if (Physics.Raycast(gunPoint.position, gunPoint.forward, out hit, detectDistance)){if(hit.transform.tag == "Player"){// 已经瞄准玩家aimedAtPlayer = true;}else{aimedAtPlayer = false;}}else{aimedAtPlayer = false;}}else{canSeePlayer = false;}}else{canSeePlayer = false;}
        // 没有在BailedOut状态下敌人才能行动if(!isBailedOut){if(canSeePlayer || isInfoShared){// 看得到玩家或有队友的信息共享时才会尝试瞄准玩家Vector3 aimPosition = aimmingPosition.position;Vector3 turretPos = transform.InverseTransformPoint(aimPosition);turretPos.y = 0f;  //过滤掉y轴的信息,防止炮塔出现绕x,z轴旋转的问题Vector3 LocalVec2Target = turretPos;Quaternion aimRotTurret = Quaternion.RotateTowards(turret.localRotation,Quaternion.LookRotation(turretPos), Time.deltaTime * rotateSpeed);turret.localRotation = aimRotTurret;Vector3 localTargetPos = turret.InverseTransformPoint(aimPosition);localTargetPos.x = 0f;Vector3 clampedLocalVec2Target = localTargetPos;if (localTargetPos.y >= 0.0f)clampedLocalVec2Target = Vector3.RotateTowards(Vector3.forward, localTargetPos, Mathf.Deg2Rad * elevation, float.MaxValue);elseclampedLocalVec2Target = Vector3.RotateTowards(Vector3.forward, localTargetPos, Mathf.Deg2Rad * depression, float.MaxValue);Quaternion rotationGoal = Quaternion.LookRotation(clampedLocalVec2Target);Quaternion aimRotGun = Quaternion.RotateTowards(gun.localRotation, rotationGoal, Time.deltaTime * rotateSpeed);gun.localRotation = aimRotGun;// 瞄准玩家并且装填完毕就会向玩家开火if (isLoaded && aimedAtPlayer){Instantiate(shell, gunPoint.position, gunPoint.rotation);this.GetComponent<Rigidbody>().AddForceAtPosition(-gunPoint.forward * reactionForce, gunPoint.position, ForceMode.Impulse);GameObject fireFXTemp = Instantiate(fireFX, gunPoint.position, gunPoint.rotation);GameObject.Destroy(fireFXTemp, 3f);fireAudioPlayer.Play();startLoading = true;isLoaded = false;}}// 装填弹药if (startLoading){Invoke("Loaded", LoadTime);startLoading = false;}}else{this.GetComponent<EnemyController>().enabled = false;}

敌人的瞄准代码就是玩家的瞄准代码,将玩家炮塔位置作为aimPosition即可。
如果有什么要优化的,我会在敌人瞄准玩家持续几秒之后再开火,而不是“甩狙”。
效果展示:敌人的索敌,瞄准,开火
值得一提的是,当敌人的坦克瘫痪时,这些都不会进行。连带着不会进行的还有EnemyController脚本,该脚本负责敌人的移动。

敌人的移动,之前想着用Unity自带的NavMeshAgent,但那个效果不真实,所以最后决定自己做。原理是:我给它一个巡逻点,他会先转向巡逻点,再旋转的“差不多了”以后就会向前移动,直到位置“差不多”达到巡逻点的位置,移动就结束了。可以设计多个巡逻点来实现多点巡逻,无法自动避障,没有设计倒车因为没有需要用到倒车的场合。
TankController脚本如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;public class EnemyController : MonoBehaviour
{//坦克左边的所有轮子public GameObject[] LeftWheels;//坦克右边的所有轮子public GameObject[] RightWheels;//坦克左边的履带public GameObject LeftTrack;//坦克右边的履带public GameObject RightTrack;// 坦克刚体public Rigidbody rb;// 旋转速度public float rotateSpeed = 0.3f;// 移动速度public float moveSpeed = 3f;// 巡逻控制public float precision = 0.999f;[HideInInspector]public bool rotateComplete = false;[HideInInspector]public bool moveComplete = false;// 巡逻点public bool patrolIsActive = false; //巡逻点是否激活public Transform patrolPoint; //巡逻点,使用外部的GameObject.Transformprivate Transform patrolPointActived; //如果激活了巡逻点,这个对象将被赋值// 巡逻模式public bool activePatrolByPlayerDetected = false; //启用这个类型,敌人会在看到玩家时前往巡逻点public bool stopPatrolByPlayerDetected = false; //启用这个类型,敌人会在看到玩家时停止巡逻//public bool patrolLoop = false; //启用这个类型,敌人会在多个巡逻点循环往返//public NavMeshAgent agent;// Start is called before the first frame updatevoid Start(){rb = this.GetComponent<Rigidbody>();rotateComplete = false;moveComplete = false;}// Update is called once per framevoid Update(){// 已经抛弃了Navmesh Agent方案//agent.SetDestination(patrolPoint.position);//根据是否检测到玩家来激活巡逻点if (activePatrolByPlayerDetected) //检测到玩家时开始巡逻{if(this.GetComponent<Enemy>().canSeePlayer || this.GetComponent<Enemy>().isInfoShared){patrolIsActive = true;}}if(stopPatrolByPlayerDetected) // 检测到玩家时停止巡逻{if(this.GetComponent<Enemy>().canSeePlayer){patrolIsActive = false;}else{patrolIsActive = true;}}// 路径是否激活if(patrolIsActive){patrolPointActived = patrolPoint;}else{patrolPointActived = null;}// 执行巡逻if (patrolPointActived != null) //如果有巡逻点的话{Vector3 path = patrolPointActived.position - this.transform.position;Vector3 direction = new Vector3(path.x, 0f, path.z).normalized;// 首先转向巡逻点的方向if (transform.InverseTransformDirection(direction).z <= precision){rotateComplete = false;float leftOrRight = Mathf.Sign(Vector3.Cross(transform.forward, direction).y); //左转还是右转?// 实际旋转Quaternion turnRotation = Quaternion.Euler(0f, leftOrRight * rotateSpeed * Time.deltaTime, 0f);rb.MoveRotation(rb.rotation * turnRotation);//坦克左右两边车轮转动foreach (var wheel in LeftWheels){wheel.transform.Rotate(new Vector3(leftOrRight * rotateSpeed * 0.06f, 0f, 0f));}foreach (var wheel in RightWheels){wheel.transform.Rotate(new Vector3(-leftOrRight * rotateSpeed * 0.06f, 0f, 0f));}//履带滚动效果// 左右LeftTrack.transform.GetComponent<MeshRenderer>().material.mainTextureOffset += new Vector2(0, -leftOrRight * 0.06f * rotateSpeed * Time.deltaTime);RightTrack.transform.GetComponent<MeshRenderer>().material.mainTextureOffset += new Vector2(0, leftOrRight * 0.06f * rotateSpeed * Time.deltaTime);}else{rotateComplete = true;}// 旋转完成后再移动坦克if (rotateComplete && path.sqrMagnitude >= 2f){moveComplete = false;// 实际移动rb.MovePosition(rb.position + transform.forward * moveSpeed * Time.deltaTime);// 车轮向前foreach (var wheel in LeftWheels){wheel.transform.Rotate(new Vector3(rotateSpeed * 0.06f, 0f, 0f));}foreach (var wheel in RightWheels){wheel.transform.Rotate(new Vector3(rotateSpeed * 0.06f, 0f, 0f));}// 履带向前LeftTrack.transform.GetComponent<MeshRenderer>().material.mainTextureOffset += new Vector2(0, 0.1f * -rotateSpeed * Time.deltaTime);RightTrack.transform.GetComponent<MeshRenderer>().material.mainTextureOffset += new Vector2(0, 0.1f * -rotateSpeed * Time.deltaTime);}else{moveComplete = true;}}}
}

是否旋转完成通过patrolPointActived.position - this.transform.position来得到坦克到巡逻点的向量,在不关注Y值的情况下对向量标准化,当这个标准化向量的z值为1时说明坦克此时正对着巡逻点。但是由于旋转往往不能正好旋转到正对着,所以用了一个precision来定一个大概的精度,可以在当z值小于precision时将旋转直接设置为正对巡逻点。向前移动因为类似问题也使用了类似技巧。使用了两个bool对象rotateComplete和moveComplete来标记运动状态,两个都为true则运动完成。patrolIsActive用于标记坦克有没有激活巡逻功能,还有注意判断坦克到底有没有巡逻点的引用。
我还设计了两种行为模式,一种会在看见玩家时才发起巡逻,另一种会在发现玩家时停止巡逻。
除了判定巡逻有没有结束,脚本还要判断敌人是怎么移动的,也就是要向左转还是向右转。通过叉乘方法以及数学方法来判断,具体也就是通过Mathf.Sign(Vector3.Cross(transform.forward, direction).y)的返回值正负来判断方向。得到的方向会用来应用到坦克的实际旋转以及车轮和履带的运动效果。

巡逻点并不仅仅是一个点,他也负责保存下一个巡逻点的信息,这样巡逻点和巡逻点可以像链表一样串起来,以此来实现多点巡逻。
PatrolPoint脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class PatrolPoint : MonoBehaviour
{public EnemyController enemyController = null;public Transform nextPatrolPoint = null;// Update is called once per framevoid Update(){// 如果。。。if (enemyController != null &&  //当该巡逻点会被某个敌人使用enemyController.patrolIsActive && //敌人激活了巡逻enemyController.patrolPoint == this.transform && //敌人目前正在行驶向该巡逻点enemyController.rotateComplete && //敌人已经完成转向enemyController.moveComplete //敌人已经完成向前移动) {if (nextPatrolPoint != null){// 将下一个巡逻点设置给敌人enemyController.patrolPoint = nextPatrolPoint;enemyController.moveComplete = false;enemyController.rotateComplete = false;}else{// 如果没有下一个巡逻点,就保持这设置为该巡逻点enemyController.patrolPoint = this.transform;}}}}

这个脚本随便写的,我觉得在EnemyController脚本中进行是否巡逻完毕的判断要好一点。
敌人巡逻效果展示:敌人的巡逻

敌人坦克的状态和玩家坦克的状态差不多,以下是Enemy脚本中的参数

    public EnemyDestoryed tankDestoryed;// 炮塔旋转public float rotateSpeed; //炮塔旋转速度public Transform turret;public Transform gun;public Transform watchTower;[Range(0.0f, 90.0f)]public float elevation = 25f;[Range(0.0f, 90.0f)]public float depression = 10f;[HideInInspector]public bool canSeePlayer; // 能否看见玩家?[HideInInspector]public bool isInfoShared; // 是否有信息共享// Bailed Out特效public GameObject bailedOutFireFx;public Transform bialedOutFireTrans;public float bailedOutTime = 10f;[HideInInspector]public bool isBailedOut = false;[HideInInspector]public bool startBailedOut = false; //专门用来启动Invoke// 敌人的装甲[HideInInspector]public float armor = 3f;// 敌人的瞄准public float detectDistance = 200f;public Transform aimmingPosition; // 瞄准玩家的点(炮塔)// 敌人开火public GameObject shell;public Transform gunPoint;public float reactionForce = 10f;public float LoadTime = 20f; //装填时间public GameObject fireFX;public AudioSource fireAudioPlayer;private bool isLoaded; //用于标记是否装填完成private bool startLoading; //用于标记是否要进行装填private bool aimedAtPlayer; //是否已瞄准?

本来想用多做点的,奈何时间和水平均不允许。

关于敌人,我还设计了一个敌人共享信息系统,代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;// 当一个敌人发现玩家时,该系统内的所有敌人相当于都发现了玩家
public class EnemyInformationSharing : MonoBehaviour
{public Enemy[] enemies;private bool someCanSee;// Start is called before the first frame updatevoid Start(){someCanSee = false;}// Update is called once per framevoid Update(){if(enemies != null){foreach (var enemy in enemies){if (enemy.canSeePlayer){someCanSee = true;break;}}if (someCanSee){foreach (var enemy in enemies){enemy.isInfoShared = true;}}}}
}

当enemies中有一个人发现了玩家,所以其他敌人的enemy.isInfoShared也会被设置为true。用来制作游戏里的“共享仇恨”效果。在第一个关卡中,最后的4辆敌军坦克就是“共享仇恨”的,他们都是看到玩家就会开始巡逻的模式,在共享下一个敌人看到了玩家也就相当于4个都看到了。


炮弹模块

“炮弹模块负责处理炮弹的碰撞检测,以及火力测试”
炮弹的碰撞检测使用OnCollisionEnter,炮弹的碰撞检测模式为Continuous Dynamic。

火力测试,顾名思义,就是炮弹能造成的伤害测试,本项目没有使用坦克世界和战争雷霆的计算等效装甲和炮弹穿深那套拟真系统,而是采用了桌游:战火FOW 的那套掷骰子系统。当炮弹打中目标时,火力测试会拿到一个随机数,火力值,如果火力值小于装甲,则这一次攻击没有产生影响,如果大于装甲值小于击毁值则坦克陷入瘫痪,如果大于击毁值则坦克击毁,大于弹药殉爆值则坦克殉爆。
Shell代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Shell : MonoBehaviour
{public float force = 8000f;public GameObject impactFX;private Rigidbody m_rb;// 火力测试private TankDestoryed.DamageType FirePowerTest(Enemy enemy){TankDestoryed.DamageType damage = TankDestoryed.DamageType.NoEffect;float firePower = Random.Range(2f, 7f);if (firePower < enemy.armor){damage = TankDestoryed.DamageType.NoEffect;}else{if (firePower < enemy.armor + 1){damage = TankDestoryed.DamageType.BailedOut;}else{if (firePower <= enemy.armor + 3){damage = TankDestoryed.DamageType.Destoryed;}else{if (firePower > enemy.armor + 3)damage = TankDestoryed.DamageType.AmmoDetonation;}}}return damage;}void Start(){m_rb = this.GetComponent<Rigidbody>();m_rb.velocity = this.transform.forward * force;Destroy(this.gameObject, 10f);}void FixedUpdate () {Debug.DrawLine(this.transform.position,this.transform.position + transform.forward,Color.red, 0.1f);}private void OnCollisionEnter(Collision collision){if(collision.gameObject.tag == "Enemy"){Enemy enemy = collision.gameObject.GetComponent<Enemy>();// 伤害判定在炮弹脚本中进行TankDestoryed.DamageType damage = FirePowerTest(enemy);switch (damage){case TankDestoryed.DamageType.NoEffect:Debug.Log("NoEffect");break;case TankDestoryed.DamageType.BailedOut:enemy.startBailedOut = true;enemy.isBailedOut = true;Debug.Log("BailedOut");break;case TankDestoryed.DamageType.Destoryed:enemy.tankDestoryed.isTankDestoryed = true;enemy.tankDestoryed.isAmmoDetonation = false;enemy.tankDestoryed.destoryedTank = enemy.transform;break;case TankDestoryed.DamageType.AmmoDetonation:enemy.tankDestoryed.isTankDestoryed = true;enemy.tankDestoryed.isAmmoDetonation = true;enemy.tankDestoryed.destoryedTank = enemy.transform;enemy.tankDestoryed.destoryedTankTurretTrans = enemy.turret;break;}if(damage == TankDestoryed.DamageType.NoEffect){// 没有产生效果,炮弹不能立刻删除Destroy(this.gameObject, 0.5f);}else{// 产生了效果,炮弹立刻删除Destroy(this.gameObject);}}else{Destroy(this.gameObject, 0.5f);}// 产生了效果 生成冲击特效GameObject temp = Instantiate(impactFX, collision.contacts[0].point, Quaternion.FromToRotation(Vector3.up, collision.contacts[0].normal));Destroy(temp, 3f);}
}

损毁模块

“损毁模块主要负责坦克的销毁功能。”
坦克的损毁由挂载到场景中的GameManager对象的特定脚本来实现。其中敌人的坦克和玩家的坦克用不同的脚本来摧毁。摧毁过程是:首先判断是否为弹药殉爆,然后根据类型生成摧毁后的残骸,最后销毁游戏对象。
弹药殉爆和普通的摧毁不同,弹药殉爆会产生两个游戏对象,一个是对应坦克的炮塔,一个是对应坦克的车身,同时对炮塔施加一个向上的力。来达到坦克弹药殉爆的视觉效果。

坦克损毁类型:

    // 坦克损坏类型public enum DamageType { NoEffect, BailedOut, Destoryed, AmmoDetonation };

玩家坦克被摧毁:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class PlayerDestoryed : MonoBehaviour
{public Player player;public GameObject TankBody_AmmoDetonated;public GameObject TankDestoryed;public GameObject TankTurret_AmmoDetonated;public GameObject TPCamera;public float ammoDetonateForce = 800f;[HideInInspector]public bool isDestoryed;[HideInInspector]public bool isAmmoDetonation = false;private void Start(){player = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>();isDestoryed = false;}private void Update(){if(isDestoryed){TPCamera.GetComponent<Camera>().enabled = true;}}// Update is called once per framevoid LateUpdate(){if(isDestoryed){DestoryPlayer();}}private void DestoryPlayer(){if (isAmmoDetonation){Transform body = player.transform;Instantiate(TankBody_AmmoDetonated, body.position, body.rotation);// 殉爆的炮塔受力受随机数影响Instantiate(TankTurret_AmmoDetonated,player.turret.position,player.turret.rotation).GetComponent<Rigidbody>().AddForce((Vector3.up + new Vector3(Random.Range(-0.05f, 0.05f), 0, Random.Range(-0.05f, 0.05f)))* Random.Range(ammoDetonateForce * 0.8f, ammoDetonateForce * 1.2f),ForceMode.Impulse);}else{Instantiate(TankDestoryed, player.transform.position, player.transform.rotation);}Destroy(player.gameObject);//isDestoryed = false;}
}

敌人坦克被摧毁:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class EnemyDestoryed : MonoBehaviour
{// 是否有坦克被击毁public bool isTankDestoryed = false;// 是否为殉爆public bool isAmmoDetonation = false;public float ammoDetonateForce = 800f;[HideInInspector]public Transform destoryedTank;[HideInInspector]public Transform destoryedTankTurretTrans;// 坦克残骸预制体public GameObject currentTankBody_AmmoDetonated;public GameObject currentTankDestoryed;public GameObject currentTankTurret_AmmoDetonated;// Start is called before the first frame updatevoid Start(){isTankDestoryed = false;isAmmoDetonation = false;destoryedTank = null;}// Update is called once per framevoid Update(){if (isTankDestoryed){if (isAmmoDetonation){Transform body = destoryedTank.transform;Instantiate(currentTankBody_AmmoDetonated, body.position, body.rotation);// 殉爆的炮塔受力受随机数影响Instantiate(currentTankTurret_AmmoDetonated,destoryedTankTurretTrans.position,destoryedTankTurretTrans.rotation).GetComponent<Rigidbody>().AddForce((Vector3.up + new Vector3(Random.Range(-0.05f, 0.05f), 0, Random.Range(-0.05f, 0.05f)))* Random.Range(ammoDetonateForce * 0.8f, ammoDetonateForce * 1.2f),ForceMode.Impulse);Destroy(destoryedTank.gameObject);isAmmoDetonation = false;}else{Instantiate(currentTankDestoryed, destoryedTank.position, destoryedTank.rotation);Destroy(destoryedTank.gameObject);}isTankDestoryed = false;destoryedTank = null;}}
}

效果展示:击毁效果


Scene模块

Scene模块主要负责场景里的UI管理,关卡管理,游戏的暂停和退出。
主要是游戏开始,关卡管理,退出游戏,游戏暂停,很简单,写的很烂,看看就行。
Level脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;// 关卡管理器
public class Level : MonoBehaviour
{public GameObject gameOverUI;public GameObject MissionCompleteUI;public GameObject gamePauseUI;public string currentScene;public string nextScene;private bool isMissionComplete;private bool isGamePaused;private void Start(){isMissionComplete = false;isGamePaused = false;}void Update(){// 判断任务失败条件if (this.GetComponent<PlayerDestoryed>().isDestoryed){gameOverUI.SetActive(true);if (Input.GetKey(KeyCode.Space)){SceneManager.LoadScene(currentScene);}}// 判断任务完成条件if(GameObject.FindGameObjectWithTag("Enemy") == null) // 敌人已被消灭完全{isMissionComplete = true;}// 加载下一关if(isMissionComplete){MissionCompleteUI.SetActive(true);if (Input.GetKey(KeyCode.Space)){SceneManager.LoadScene(nextScene);}}// 游戏暂停中if (isGamePaused){if (Input.GetKeyDown(KeyCode.Escape)){Debug.Log("Quit");Application.Quit();}if (Input.GetKey(KeyCode.Space)){isGamePaused = false;gamePauseUI.SetActive(false);Time.timeScale = 1f;}}// 游戏暂停判断if (Input.GetKey(KeyCode.Escape)){isGamePaused = true;gamePauseUI.SetActive(true);Time.timeScale = 0;}}
}

游戏在玩家被击毁时按ESC键不会退出游戏,而是进入暂停,这是个BUG。


好了,如果你看到这里,感谢您看完了这篇博客。我是一名努力成为游戏开发者的大三学生。欢迎关注我。

一个Unity3D制作的坦克游戏——《燃烧的地平线》相关推荐

  1. html5坦克游戏ppt说明,HTML5制作的坦克游戏

    HTML5制作的坦克游戏 本游戏是基于HTML5开发的 网页游戏,js,css辅助开发 源码如下: 坦克大战 坦克大战(请使用IE浏览器) style="background-color:b ...

  2. html坦克游戏,HTML5制作的坦克游戏

    HTML5制作的坦克游戏 本游戏是基于HTML5开发的 网页游戏,js,css辅助开发 源码如下: 坦克大战 坦克大战(请使用IE浏览器) style="background-color:b ...

  3. 使用unity3d制作像素鸟游戏

    个人博客文章链接:http://www.huqj.top/article?id=140 unity3d虽然是被设计用来制作3D游戏的,但是它提供了很多2D组件,所以也可以轻松的设计制作2D游戏,下面记 ...

  4. unity3d制作自己简单游戏场景

    下面演示一下如何用unity3d制作一个自己的游戏场景 1. 导入环境资源 Assets -> import packet -> environments 2. 新建terrain 3.在 ...

  5. Unity3d制作简单拼图游戏

    本文为原创,如需转载请注明原址:http://blog.csdn.net/cube454517408/article/details/7907247 最近一直在使用Unity,对它有了一些小的使用心得 ...

  6. Linux数独小游戏C语言,发一个自己制作的数独游戏代码!

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 void draw(int i,int j) {setcolor(5); rectangle(45+j*30,45+i*30,36+(j+1)*30,36 ...

  7. 如何使用cocos2dx 制作一个多向滚屏坦克类射击游戏-第二部分

    原文链接:http://www.raywenderlich.com/6888/how-to-make-a-multi-directional-scrolling-shooter-part-2 这里使用 ...

  8. (目录)[0]尝试用Unity3d制作一个王者荣耀(持续更新)-游戏规划

    太得闲了于是想写个农药,虽然可能会失败但是还是要试一试. 因为是自学的不是Unity专业的可能表达语言会有些不标准!望见谅! 结构: 以组件式(比如说摇杆控制和玩家部分的编写是分离的,可以自由拼装)作 ...

  9. 用python做一个坦克小游戏_Python制作经典坦克大战小游戏

    image.png 开发工具 Python版本:3.6.4 相关模块: pygame模块: 以及一些Python自带的模块. 环境搭建 安装Python并添加到环境变量,pip安装需要的相关模块即可. ...

  10. 【跟我一起学Unity3D】做一个2D的90坦克大战之AI系统

    对于AI,我的初始想法非常easy,首先他要能动,而且是在地图里面动. 懂得撞墙后转弯,然后懂得射击,其它的没有了,基于这个想法,我首先创建了一个MyTank类,用于管理玩家的坦克的活动,然后创建AI ...

最新文章

  1. LetCode 15 三数之和
  2. 【论文解读】KDD20 | 图神经网络在生物医药领域的应用
  3. 使用 fail2ban 防御 SSH 暴力破解
  4. Postman测试接口传入List类型的参数以及数组类型参数
  5. php缓存页面,PHP缓存页面函数的简单示例
  6. LeetCode 59. Spiral Matrix II
  7. android NDK 详解
  8. html5人脸登录,基于HTML5 的人脸识别活体认证
  9. linux下配置nginx+rtmp+obs推流
  10. Binder机制(非常好理解)
  11. 使用全局优化方法识别中文事件因果关系
  12. 要建立亲密的关系,就必须少一些指责,多一些倾听
  13. 封装一个简单实用的朋友圈
  14. Model和ModelMap的关系
  15. 计算机 班徽设计大赛,班徽设计大赛来啦!
  16. ventory做U盘启动,使用vmware进行测试U盘系统盘是否制作成功
  17. 小白如何学习运营公众号?
  18. 2016年蓝桥杯A组 第九题 密码脱落
  19. 【Python Sympy】将表达式化为关于x的多项式,求出多项式系数
  20. firefox的一些插件~

热门文章

  1. 免流发展史-三大运营商
  2. 路由器回执路由配置_IT菜鸟之路由器基础配置(静态、动态、默认路由)
  3. Ubuntu(Linux)使用微信的方法
  4. CSDN万粉,老师初体验 | 我迟来的2021年S3总结
  5. rhino java api demo_用 Rhino 脚本化 Java
  6. tif格式文件用什么打开(如何打开怎么打开) tif是什么格式文件 ...
  7. @kubernetes(k8s)使用adm安装实现keepalived高可用
  8. mysql表文件被删除,MySQL数据表InnoDB引擎表文件误删恢复
  9. 【论文泛读】Leveraging Distribution Alignment via Stein Path for Cross-Domain Cold-Start Recommendation
  10. 一加nfc门禁卡录入_一加7t怎么开启NFC 模拟门禁卡方法介绍