车辆多视角3D漫游系统(中山大学3D游戏大作业)
- 一、简介
- 二、项目结构
- 1.UML
- 2.script
- 三、代码分析
- 四、参考文章
github仓库:https://github.com/linfn3/game_car b站视频:https://www.bilibili.com/video/BV1GP4y1C7bs/
一、简介
利用粒子系统、物理系统、UI系统等技术以多款赛车为载体,设置多视角,实现多场景身临其境的车辆漫游,以及人物交互、天气系统、常用游戏界面、小地图等功能
二、项目结构
1.UML
2.script
三、代码分析
游戏场景:
粒子系统:
在Dust_Thresh.cs文件中使用粒子系统。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Dust_Thresh : MonoBehaviour
{
// 指定车辆的刚体和烟尘粒子系统
public Rigidbody carRigidbody;
public ParticleSystem dustParticles;
// 定义一个阈值,用于控制烟尘效果的播放
public float dustThreshold = 45f;
void Update()
{
// 如果车辆的速度超过阈值,则播放烟尘效果
if (carRigidbody.velocity.magnitude > dustThreshold)
{
dustParticles.Play();
}
else
{
dustParticles.Stop();
}
}
}
车辆漫游:
具体代码写于Car_Control_opm.cs,包括运动的控制、速度设置、调节引擎音效等功能。
常见问题:
位移突兀,没有摩擦阻力;速度无限制,容易穿模;急刹车时位移和速度突变为0
解决方法:
使用Input.GetKey()函数实时捕捉键盘wasd键按键状态,判断速度边界,使用加速度改变车辆实时速度,从而实现位移的平稳过渡;刹车时,使用Math .Lerp函数对速度按时间Δt进行内插,达到平稳过渡效果。
车轮运动通过获取车轮碰撞体的物理转动(轮轴转速、摆动角度),使得车轮mesh模型随之旋转
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Car_Control_opm : MonoBehaviour
{
private float y;
//车辆控制速度参数
public float speedOne = 0f; //车辆实时速度
private float speedMax = 50f/2.8f; //车辆最大速度
private float speedMin = -30f; //车辆最小速度(倒车最大速度)
private float speedUpA = 3f; //车辆加速加速度(A键控制)
private float speedDownS = 4f; //车辆减速加速度(S键控制)
private float speedTend = 0.5f; //无操作实时速度趋于0时加速度
private float speedBack = 1f; //车辆倒车加速度
//引擎音效范围
public float minVolume = 0.5f; // 引擎声音的最小音量
public float maxVolume = 0.85f; // 引擎声音的最大音量
public float minPitch = 0.4f; // 引擎声音的最小音调
public float maxPitch = 1.2f; // 引擎声音的最大音调
public AudioClip CarSound;
private bool isIgnited = false;
private MeshRenderer[] wheelMesh;
private WheelCollider[] wheel;
private float h;
private float v;
float maxAngle = 35;//最大转角
public ParticleSystem N2Particles;
void Start()
{
wheelMesh = transform.GetChild(2).GetComponentsInChildren<MeshRenderer>();
wheel = transform.GetChild(3).GetComponentsInChildren<WheelCollider>();
}
void Update()
{
if (Input.GetKey(KeyCode.LeftShift))
{
N2Particles.Play();
speedMax = 50f / 2.8f * 1.2f;
speedUpA = 6f;
}
else
{
N2Particles.Stop();
speedMax = 50f / 2.8f;
speedUpA = 3f;
}
// 判断是否按下 B 键
if (Input.GetKeyDown(KeyCode.B))
{
// 如果已经点火,则忽略
if (isIgnited)
{
return;
}
// 点火
isIgnited = true;
}
// 判断是否已经点火
if (!isIgnited)
{
return;
}
AudioSource audioSource = GetComponent<AudioSource>();
audioSource.clip = CarSound;
// 计算引擎声音的音量
float volume = Mathf.Lerp(minVolume, maxVolume, speedOne / 80.0f);
// 设置引擎声音的音量
audioSource.volume = volume;
// 计算引擎声音的音调
float pitch = Mathf.Lerp(minPitch, maxPitch, speedOne / 100.0f);
// 设置引擎声音的音调
audioSource.pitch = pitch;
//按下W键并且速度没达到最大,则速度增加
if (Input.GetKey(KeyCode.W) && speedOne < speedMax)
{
speedOne = speedOne + Time.deltaTime * speedUpA;
}
//按下S键并且速度没达到零,则速度减小
if (Input.GetKey(KeyCode.S) && speedOne > 0f)
{
speedOne = speedOne - Time.deltaTime * speedDownS;
}
//没有执行速度操作并且速度大于最小速度,则缓慢操作
if (!Input.GetKey(KeyCode.W) && !Input.GetKey(KeyCode.S) && speedOne > 0f)
{
speedOne = speedOne - Time.deltaTime * speedTend;
}
if (!Input.GetKey(KeyCode.W) && !Input.GetKey(KeyCode.S) && speedOne < 0f)
{
speedOne = speedOne + Time.deltaTime * speedTend;
}
//按下S键并且速度没有达到倒车速度最大时,且车辆处于可以倒车状态时车辆倒车
if (Input.GetKey(KeyCode.S) && speedOne > speedMin && speedOne <= 0)
{
speedOne = speedOne - Time.deltaTime * speedBack;
}
//按下空格,则汽车停止
if (Input.GetKey(KeyCode.Space) && speedOne != 0)
{
speedOne = Mathf.Lerp(speedOne, 0, Time.deltaTime * 2f);
if (speedOne < 2f) speedOne = 0;
}
transform.Translate(Vector3.forward * speedOne * Time.deltaTime);
//使用A和D来控制物体左右旋转
if (speedOne > 1f || speedOne < -1f)
{
y = Input.GetAxis("Horizontal") * 60f * Time.deltaTime;
transform.Rotate(0, y, 0);
}
h = Input.GetAxis("Horizontal");
v = Input.GetAxis("Vertical");
if (0 == Mathf.Abs(h) && 0 == Mathf.Abs(v)) return;
else
{
for (int i = 0; i < 2; i++)
{
wheel[i].steerAngle = h * maxAngle;
}
for (int i = 0; i < 4; i++)
{
wheelMesh[i].transform.localRotation = Quaternion.Euler(wheel[i].rpm * 360 / 60, wheel[i].steerAngle, 0);
}
}
}
}
相机跟随设置:Follow.cs
相机跟随的核心思想:让相机始终和汽车中心保持有平滑度的距离差,而非固定的距离差,通过内插实现,而非固定
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Follow : MonoBehaviour
{
private Transform targetPos;//跟随的目标
private Vector3 offsetPos;//固定位置
private Vector3 tempPos;//内插出来的位置
private bool is_follow = false;
// Start is called before the first frame update
void Start()
{
offsetPos = new Vector3 (1, 2, -4);
targetPos = GameObject.FindGameObjectWithTag("Player").transform;
}
// Update is called once per frame
void FixedUpdate()
{
if(Input.GetKey(KeyCode.B))
{
is_follow = true;
}
if(is_follow)
{
tempPos = targetPos.position + targetPos.TransformDirection(offsetPos);//坐标转为车辆朝向方向
transform.position = Vector3.Lerp(transform.position, tempPos, Time.fixedDeltaTime * 1.8f);//内插使得摄像机平滑跟随
transform.LookAt(targetPos);//使得摄像机始终望向车辆
}
}
}
音效播放通过brake.cs处理
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Brake : MonoBehaviour
{
// 刹车音效
public AudioClip BrakeSound;
public ParticleSystem speckleParticles;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
AudioSource audioSource = GetComponent<AudioSource>();
// 判断是否按下 Space 键
if (Input.GetKey(KeyCode.Space))
{
// 播放点火音效
speckleParticles.Play();
audioSource.clip = BrakeSound;
audioSource.Play();
}
else {
speckleParticles.Stop();
audioSource.Stop();
}
}
}
PauseMenu.cs控制菜单
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class PauseMenu : MonoBehaviour
{
public static bool GameIsPaused = false;
public GameObject pauseMenuUI;
void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
if (GameIsPaused)
{
Resume();
}
else
{
Pause();
}
}
else
{
Debug.Log("Back key not pressed");
}
}
public void Resume()
{
pauseMenuUI.SetActive(false);
Time.timeScale = 1.0f;
GameIsPaused = false;
}
public void Pause()
{
pauseMenuUI.SetActive(true);
Time.timeScale = 0.0f;
GameIsPaused = true;
}
public void MainMenu()
{
Time.timeScale = 1.0f;
GameIsPaused = false;
SceneManager.LoadScene("StartMenu");
}
public void QuitGame()
{
Debug.Log("QUIT!");
Application.Quit();
}
}
Ignite.cs,用于车辆点火
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Ignite : MonoBehaviour
{
// 是否已经点火
private bool isIgnited = false;
// 点火音效
public AudioClip ignitionSound;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
// 判断是否按下 B 键
if (Input.GetKeyDown(KeyCode.B))
{
// 如果已经点火,则忽略
if (isIgnited)
{
return;
}
// 点火
isIgnited = true;
// 播放点火音效
AudioSource audioSource = GetComponent<AudioSource>();
audioSource.clip = ignitionSound;
audioSource.Play();
}
// 判断是否已经点火
if (!isIgnited)
{
return;
}
}
}
View_Change.cs用于变换视角,按下键盘2代表驾驶员,4代表副驾驶,3代表旁观者
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class View_Change : MonoBehaviour
{
// 指定第一人称摄像机和第三人称摄像机
public Camera First_Person_View;
public Camera Third_Person_View;
public Camera Driver_View;
public Camera Side_View;
// Start is called before the first frame update
void Start()
{
First_Person_View.enabled = false;
Driver_View.enabled = false;
Third_Person_View.enabled = true;
Side_View.enabled = false;
}
// 定义一个标志变量,用于指示当前是否为第一人称视角
void Update()
{
// 如果按下了1键,则切换为第一人称
if (Input.GetKeyDown(KeyCode.Alpha1))
{
SwitchView1();
}
// 如果按下了2键,则切换为driver
if (Input.GetKeyDown(KeyCode.Alpha2))
{
SwitchView2();
}
// 如果按下了3键,则切换为第三人稱
if (Input.GetKeyDown(KeyCode.Alpha3))
{
SwitchView3();
}
// 如果按下了4键,则切换为Side
if (Input.GetKeyDown(KeyCode.Alpha4))
{
SwitchView4();
}
}
void SwitchView1()
{
First_Person_View.enabled = true;
Third_Person_View.enabled = false;
Driver_View.enabled = false;
Side_View.enabled = false;
}
void SwitchView2()
{
First_Person_View.enabled = false;
Third_Person_View.enabled = false;
Driver_View.enabled =true ;
Side_View.enabled = false;
}
void SwitchView3()
{
First_Person_View.enabled = false;
Third_Person_View.enabled =true ;
Driver_View.enabled = false;
Side_View.enabled = false;
}
void SwitchView4()
{
First_Person_View.enabled = false;
Third_Person_View.enabled = false;
Driver_View.enabled = false;
Side_View.enabled = true;
}
}
weather.cs天气系统,可以实现晴天、雨天、阴天、下雪天变换
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class weather : MonoBehaviour
{
public Material[] mats;
// Start is called before the first frame update
void Start()
{
RenderSettings.skybox = mats[0];
}
// public ParticleSystem clearWeather; // 晴天效果
// public ParticleSystem cloudWeather;//阴天效果
public ParticleSystem rainWeather; // 下雨天效果
public ParticleSystem snowWeather; // 下雪天效果
// Update is called once per frame
void Update()
{
// 监听数字键盘的“F1”、“F2”、“F3”、“F4”按键
if (Input.GetKeyDown(KeyCode.F1))
{
// 如果按下“F1”键,则切换到晴天效果
RenderSettings.skybox = mats[0];
rainWeather.gameObject.SetActive(false);
snowWeather.gameObject.SetActive(false);
}
else if (Input.GetKeyDown(KeyCode.F2))
{
// 如果按下“F2”键,则切换到阴天效果
Debug.Log("Back key not pressed");
RenderSettings.skybox = mats[1];
rainWeather.gameObject.SetActive(false);
snowWeather.gameObject.SetActive(false);
}
else if (Input.GetKeyDown(KeyCode.F3))
{
// 如果按下“F3”键,则切换到下雨天效果
RenderSettings.skybox = mats[1];
rainWeather.gameObject.SetActive(true);
snowWeather.gameObject.SetActive(false);
}
else if (Input.GetKeyDown(KeyCode.F4))
{
// 如果按下“F4”键,则切换到下雪天效果
RenderSettings.skybox = mats[1];
rainWeather.gameObject.SetActive(false);
snowWeather.gameObject.SetActive(true);
}
}
}
晴天:
阴天:
Minimap.cs控制小地图
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MiniMap : MonoBehaviour
{
// Start is called before the first frame update
public Camera minicamera;//小地图对应的摄影机
public Transform player;//赛车对应的位置
public Transform miniplayerIcon;//赛车对应的箭头的位置
void Start()
{
}
// Update is called once per frame
void Update()
{
//将游戏对象上方确定距离的位置赋值给小地图的相机位置
minicamera.transform.position = new Vector3(player.position.x,
minicamera.transform.position.y, player.position.z);
//将箭头指向游戏对象的正向使其随游戏对象移动
miniplayerIcon.eulerAngles = new Vector3(0, 0, -player.eulerAngles.y);
}
}