遗落的星球
1 大作业内容简介
三个多世纪前,天体物理学家们发现太阳的演化将产生一次叫氦闪的剧烈爆炸,变成一颗巨大且暗红的巨星,地球将在这次氦闪中被汽化。这一切将在四百年内发生,现在已过了三百八十年。太阳的灾变将炸毁和吞没太阳系所有适合居住的类地行星。所以,人类唯一的生路,就是向外太空恒星际移民。而逃离方式一直存在争议,争论的焦点,分为飞船派和地球派。
最近,科学家们发现太阳又急速衰老膨胀,短时间内包括地球在内的整个太阳系都将被太阳所吞没。为了自救,人类提出一个名为“流浪地球”的大胆计划,即倾全球之力在地球表面建造上万座发动机和转向发动机,推动地球离开太阳系,用2500年的时间奔往新家园。
而你作为当时地球上,为数不多“宇航世家”的后代,自然而然的成为了这项活动的先驱者。在地球真正开始“流浪之前”,你驾驶着代号为AE86——当时最为先进的一批宇宙飞船,飞向目标星域进行提前探索。你身为当时地球上,至高无上的“宇航世家”的后代,原本这只是一次和平常一样的领航任务,但在飞行到艾卡西亚星球附近时,AE86号飞船突然受到不明原因的电磁波干扰。飞船失去动力坠毁,而你也被迫降落于此。千年前才有的小屋、中世纪的城堡、高耸的山川、清澈的湖水,湛蓝的天空和远处高耸入云的大门,前面又会有怎样的困境正等待着你,而你又怎么样逃离这座星球,回到地球上去。
2 游戏场景设计
2.1 素材来源
本作品大部分素材来自于Unity资源商店中的免费素材,代码大部分由自己编写,少部分参考查询到的文献和CSDN博客等,音效来源于网易云音乐网站。
2.2 创建项目
2.3 导入材质包
在Unity商店选择购买需要的资源后,可以选择在Unity中打开并进行下载,下载完成后即可导入项目中,进行使用。
2.4 创建天空盒子SkyBox
引入相关的天空盒子素材包,创建天空盒子后,将相应的材质包使用进来
使用了天空盒子后的天空效果
2.5 创建地形
2.5.1 生成地形
使用Unity自带的GameObject中的Terrain,进行相关地形的构造。
对地形进行相关参数的设置,将长度和宽度都设置成500,由于我们采用的是降低部分地形高度的方法来创造“地洞”,因为在这里将高度设置成为600,方便我们后续进行湖泊等场景的构造。
2.5.2 设置地形材质
使用引入的相关材料包,生成一些符合地形特征的材质包,并为地形使用合适的材质包
2.5.3 构造山丘
在此界面下,选中合适的笔刷,配置笔刷大小和透明度等参数后,便可使用鼠标点击地形,进行地形的拉伸抬高,构造山地的环境。
2.5.4 构造湖泊
在构造湖泊之前,我们需要在地形上创建出一个“坑”,然后在“坑”的表面,填充我们的水面,从而构造出简易湖泊的效果。
使用Paint Height工具,来进行指定高度地形的绘制
使用外部资源的水面预制体,进行表面填充,注意水面的高度要略低于地形的高度
使用水面填充后的效果
总体地形展示
2.6 细节设置
2.6.1 添加树木
引入与自然环境相关的材质包,并加入到项目中。
在地形处,可以添加我们与树木有关的笔刷,配置好笔刷大小和树木的密度之后,就可以使用笔刷进行树木的绘制
树木效果
并且树木的树叶,会随着风而摇曳,产生动态的效果。
2.6.2 添加水面光影
为了让游戏更加贴合我们的生活实际,更加真实有趣味,可以使用到类似于MineCraft中的光影效果,比如对水面施加光影效果,使得在不同光照下,水面产生不一样的光照效果,并且生活中的水面也不是静止不动的,因此需要进行相关的配置,让水面表面不仅能反射出不同的光照,以及我们天空盒子中的云彩,还要能动态的波动。
2.7 建筑构建
正如在内容简介中提到的那样,在这个星球上,会有千年前才有的小屋、中世纪的城堡、高耸的山川、清澈的湖水,湛蓝的天空和远处高耸入云的大门,因此我们需要选用合适的资源包,构建出这些具有时代特色的不同建筑。
2.7.1 木质小屋(出生点)
引入木制小屋的资源包后,调整大小和角度。
为了防止人物和小屋之间产生穿模,需要给小屋添加上碰撞盒,从而使用刚体的特性,防止物体穿过模型
2.7.2 村庄
通过相关的建模,构建如:房屋、桌子、椅子、捆柴等和村庄相关的元素,并为这些物件设置对应的材质,使其更具有真实气息
2.7.3 喷泉场景
引入喷泉物件后,在其子组件中设置制作水柱的粒子系统。
添加完喷泉组件和粒子系统后,我们需要调整粒子系统相对于喷泉组件的位置,将粒子系统摆放在喷泉的顶部,从而能制造喷泉的效果。
根据效果,调整粒子系统相关的参数。
调整持续时间为5.00秒,并开启循环播放模式。
调整粒子的速度为10~20区间中的随机值
调整粒子的大小为1~5区间中的随机值
由于需要制造类似于喷泉的效果,而日常生活中喷泉水会由于重力的作用下坠,因此我们需要给粒子系统添加上重力的作用。
而默认粒子发射的速度较慢,造成粒子数量较少,无法形成喷泉的流动性效果,因此需要将粒子系统的发射速度加快。
喷泉的效果图
2.7.4 瀑布场景
按照日常生活中的常识,瀑布会从山上的某个位置落下。因此我们需要创建一个粒子系统,并将其位置置于山上较高出。为了营造出瀑布的效果,因此我们还需要调整粒子大小随时间变化,以及最后瀑布落在底部池水中,形成的水面溅射的反弹效果
通过在这里调整粒子大小曲线,来设置粒子大小随时间的变化,从而制造粒子大小不一的效果
瀑布无论是落到地面,还是落到水面上,都会产生一定的溅射水花效果,而要模拟这样的效果,我们需要给粒子系统规定确定一个反射平面,使其碰到这个规定的平面的时候,因物理作用而产生反射的溅射水花效果。比如这里,就使用水面作为反射平面。
2.7.5 宝箱解密
本游戏较为有趣的玩法就是可以拾取宝箱,并且拾取宝箱的同时会产生一个粒子效果,表示对“拾取成功”的庆祝。
2.7.6 自动开关门
通过人物和房屋之间的碰撞检测,从而来判断人物模型是否已经到达了交互区域。
通过射线检测的方式,在人物前方不断发送射线进行检测,如果到达了交互区域,则对门播放开门动画。开门3s后,如果人物已经离开了检测有效区域,那么就自动播放关门动画。
2.8 加入角色模型
2.9 第三人称游戏视角
本游戏在视角设计上,采用第三人称视角默认,将相机挂载到人物模型的背部上,并使相机朝向人物模型的头部。
采用第一人称的主要原因是:相对于第一人称来说,第三人称能更好地观察任务模型的细节,也更便于操作。
编写第三人称视角的相关代码,主要是对相机进行操作,使相机跟随人物的移动而移动
第三人称视角部分代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ThirdPersonCamera : MonoBehaviour
{
private const float Y_ANGLE_MIN = 0.0f;
private const float Y_ANGLE_MAX = 50.0f;
public Transform lookAt;
public Transform camTransform;
public float distance = 5.0f;
private float currentX = 0.0f;
private float currentY = 45.0f;
private float sensitivityX = 20.0f;
private float sensitivityY = 20.0f;
private void Start()
{
camTransform = transform;
}
private void Update()
{
currentX += Input.GetAxis(“Mouse X”);
currentY += Input.GetAxis(“Mouse Y”);
currentY = Mathf.Clamp(currentY, Y_ANGLE_MIN, Y_ANGLE_MAX);
}
private void LateUpdate()
{
Vector3 dir = new Vector3(0, 0, -distance);
Quaternion rotation = Quaternion.Euler(currentY, currentX, 0);
camTransform.position = lookAt.position + rotation dir;
camTransform.LookAt(lookAt.position);
}
}
*角色控制相关脚本:
using UnityEngine;
using System.Collections;
public class Player : MonoBehaviour
{
private Animator anim;<br /> private CharacterController controller;
private int cnt;<br /> public AudioClip audioSource;<br /> public float speed = 600.0f;<br /> public float turnSpeed = 400.0f;<br /> private Vector3 moveDirection = Vector3.zero;<br /> public float gravity = 20.0f;<br /> private AudioSource _mAudioSource;<br /> private Rigidbody rigidBody;
public float jumpSpeed = 40f;<br /> void Start()<br /> {<br /> <br /> _mAudioSource = GetComponent<AudioSource>();<br /> rigidBody = transform.GetComponent<Rigidbody> ();<br /> rigidBody.useGravity = false;<br /> rigidBody.isKinematic = true;<br /> _mAudioSource.clip = audioSource;<br /> controller = GetComponent<CharacterController>();<br /> anim = gameObject.GetComponentInChildren<Animator>();<br /> cnt = 0;<br /> _mAudioSource.Play ();<br /> }<br /> void Update()<br /> {
if (Input.GetKey (KeyCode.R)) {<br /> gameObject.transform.position = new Vector3 (189.0631f, 250f, 371.857f);<br /> return;<br /> }<br /> if ((Input.GetKey ("w") || Input.GetKey ("s")) && Input.GetKey (KeyCode.LeftShift)) {<br /> speed = 50.0f;<br /> /*<br /> if (_mAudioSource.isPlaying == false && cnt % 60 == 0)<br /> {
_mAudioSource.Play();<br /> }<br /> */<br /> anim.SetInteger ("AnimationPar", 1);<br /> cnt++;<br /> } else if ((Input.GetKey ("w") || Input.GetKey ("s")) && !Input.GetKey (KeyCode.LeftShift)) {<br /> speed = 30.0f;<br /> anim.SetInteger ("AnimationPar", 1);<br /> cnt++;<br /> } else<br /> {<br /> _mAudioSource.Stop();<br /> anim.SetInteger("AnimationPar", 0);<br /> }
if (controller.isGrounded)<br /> {<br /> moveDirection = transform.forward * Input.GetAxis("Vertical") * speed;<br /> }
float turn = Input.GetAxis("Horizontal");<br /> transform.Rotate(0, turn * turnSpeed * Time.deltaTime, 0);<br /> rigidBody.MovePosition (moveDirection * Time.deltaTime);<br /> controller.Move(moveDirection * Time.deltaTime);<br /> if (Input.GetKeyDown (KeyCode.Space)) {<br /> anim.SetInteger ("AnimationPar", 1);
rigidBody.velocity += new Vector3 (0, 30, 0);<br /> rigidBody.AddForce (Vector3.up * jumpSpeed);<br /> }<br /> moveDirection.y -= gravity * Time.deltaTime;<br /> }<br />} <br />**角色动画状态机:**<br />![](https://cdn.nlark.com/yuque/0/2022/png/21436600/1656786989236-96db5d18-90e8-4114-b5ce-2e5f2248f646.png#averageHue=%23282625&id=yAHxR&originHeight=565&originWidth=1121&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />![](https://cdn.nlark.com/yuque/0/2022/png/21436600/1656786989698-1c0aa8f3-3300-4691-ba41-053f2968df2e.png#averageHue=%235f6740&id=BxDZv&originHeight=565&originWidth=1121&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />**第三人称走路视角**
3 游戏代码设计
3.1 小木屋开关门设计
在本场景中,有各式各样很多的建筑物,而部分木屋的门实际上是可以进行交互的,也就是说门是可以打开,人物是可以进入到木屋内部的。而这实际上,就是对木门的开关门动画进行操作,我们使用射线方式进行检测:当人物步入到检测区内部,并且和门在同一条直线上时,我们对门进行“开门”动画播放。当人物离开监测区域或者和门不在同一条直线上时,如果累计离开时间超过了3s钟,那么会判定人物已经离开,则会自动播放“关门”动画。
这样,我们就根据预设好的门上的相关动画,实现了门的自动开启和关闭。
编写碰撞检测的RayCharacterCollision脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RayCharacterCollision : MonoBehaviour {
private bool doorIsOpen = false;
private float doorTimer = 0.0f;
private float doorOpenTime = 3.0f;
// Use this for initialization
void Start () {
}
void OpenDoor(){
doorIsOpen = true;
GameObject myHouse = GameObject.Find (“house”);
myHouse.GetComponent
}
void ShutDoor(){
doorIsOpen = false;
GameObject myHouse = GameObject.Find (“house”);
myHouse.GetComponent
}
// Update is called once per frame
void Update () {
RaycastHit hit;
if (Physics.Raycast (transform.position, transform.forward, out hit, 5)) {
if (hit.collider.gameObject.tag == “houseDoor” && doorIsOpen == false) {
OpenDoor ();
}
}
if (doorIsOpen) {
doorTimer += Time.deltaTime;
if (doorTimer > doorOpenTime) {
ShutDoor ();
doorTimer = 0.0f;
}
}
}
}
编写开关门的OpenOrCloseDoorScript脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class OpenOrCloseDoorScript : MonoBehaviour {
public bool doorIsOpen = false;
private float doorTimer = 0.0f;
public float doorOpenTime = 3.0f;
void OpenDoor(){
doorIsOpen = true;
GameObject myHouse = GameObject.Find (“house”);
myHouse.GetComponent
}
void OnControllerColliderHit(ControllerColliderHit hit) {
if (hit.gameObject.tag == “houseDoor” && doorIsOpen == false) {
OpenDoor ();
}
}
// Use this for initialization
void Start () {
}
void ShutDoor(){
doorIsOpen = false;
GameObject myHouse = GameObject.Find (“house”);
myHouse.GetComponent
}
// Update is called once per frame
void Update () {
if (doorIsOpen) {
doorTimer += Time.deltaTime;
if (doorTimer > doorOpenTime) {
ShutDoor ();
doorTimer = 0.0f;
}
}
}
}
在我们house组件的door部分上,打上标签“houseDoor”,方便我们从代码中通过tag标签来获取GameObject组件
由于我们的人物是由玩家操作的,因此我们需要将上述的两个脚本都挂载到我们的人物模型上,实现和木门的碰撞检测,从而实现房门的自动开合与关闭。
可以发现,在人物距离木门比较远的时候,由于没有检测到碰撞,因此不会播放“开门”的动画,宏观上来看就是木门不会开启。而在人物较近的时候,检测到了碰撞,自动播放“开门动画”,在检测不到人物超过3s以后,木门自动播放“关门”动画,宏观上来看就是木门关闭。
3.2 巨大木门F键交互设计
首先,为什么目前很多的游戏(最典型的就是绝地求生PUBG)都采用F键与游戏物体进行交互?
这个问题每个人有自己不同的见解,也就会有不同的说法。有人认为代表的是“Function”,也就是函数、操作的意思,也有人认为只是单纯因为F键距离我们人的左手是比较近的,方便用户,或者说玩家进行操作。这两种说法都有各自的思考,都是可取的。
在本游戏中,我们需要与一扇巨大木门进行交互,进行“拆除障碍”的操作。
3.2.1 创建巨大木门
选用合适的素材,在两座山之间放置一扇木门,并通过缩放,来调整木门的大小,使得木门可以将山体之间的空隙拦住,造成“障碍”的效果。
3.2.2 碰撞检测
我们需要与木门进行交互,很显然,我们首先需要与木门进行接触,那么如何判断是否与木门接触了呢?
这就需要我们采用碰撞检测的方式:如果检测两物体产生了碰撞,那么就进行特殊的一些操作。
首先为我们上述描述中创建的木门添加碰撞器,为后续碰撞检测做准备,并将相关的脚本文件挂载到木门上。
相关的脚本代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BigDoorDestory : MonoBehaviour {
public bool isEnterF = false;
// Use this for initialization
void Start () {
}
void OnTriggerEnter(Collider collider) {
Debug.Log (“开始接触”);
if (Input.GetKeyDown (KeyCode.F)) {
Destroy (this.gameObject);
this.isEnterF = true;
}
}
void OnTriggerExit(Collider collider) {
GameObject player = GameObject.FindGameObjectWithTag (“Player”);
if (!this.isEnterF) {
player.transform.position = new Vector3 (88.749f, 250f, 185.72f);
}
this.isEnterF = false;
}
void OnTriggerStay(Collider collider) {
if (Input.GetKeyDown (KeyCode.F)) {
Destroy (this.gameObject);
this.isEnterF = true;
}
}
// Update is called once per frame
void Update () {
}
}
这样,当人物模型接触到了大门却未按F键交互的时候,会将人物重新传送到大门前,阻碍通过。而当人物按下了F键进行交互的时候,会将大门进行摧毁,从而形成摧毁障碍物的效果。
按下F后,大门摧毁消失
3.3 宝箱互动
和宝箱交互的按键同上述与大门交互的按键一致,使用F键。
在场景中添加宝箱,和对应“拾取成功”后的粒子系统
由于拾取宝箱的操作也是只有当人物和宝箱的距离较近的时候才能产生,因此我们同样需要给宝箱添加一个碰撞器,用来作碰撞检测。
编写拾取宝箱的脚本PickupContainer:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PickupContainer : MonoBehaviour {
public bool isEnterF = false;
private ParticleSystemRenderer psr;
// Use this for initialization
void Start () {
psr = this.GetComponent
}
void OnTriggerEnter(Collider collider) {
Debug.Log (“开始接触”);
if (Input.GetKeyDown (KeyCode.F)) {
Destroy (this.gameObject);
if (!isEnterF) {
GameObject.FindWithTag (“yes1”).GetComponent
}
this.isEnterF = true;
}
}
void OnTriggerStay(Collider collider) {
if (Input.GetKeyDown (KeyCode.F)) {
Destroy (this.gameObject);
if (!isEnterF) {
GameObject.FindWithTag (“yes1”).GetComponent
}
this.isEnterF = true;
}
}
// Update is called once per frame
void Update () {
}
}
为粒子系统打上Tag标签:“yes1”,并调整粒子的速度、大小和方向等参数,同时调整粒子的颜色效果,使得外观更加美观好看。
同理,在另外一片区域中,放置了一个更为隐秘的宝箱,拾取后同样会有相应的粒子效果提示。
相应的脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PickupContainer2 : MonoBehaviour {
public bool isEnterF = false;
private ParticleSystemRenderer psr;
// Use this for initialization
void Start () {
psr = this.GetComponent
}
void OnTriggerEnter(Collider collider) {
Debug.Log (“开始接触”);
if (Input.GetKeyDown (KeyCode.F)) {
Destroy (this.gameObject);
if (!isEnterF) {
GameObject.FindWithTag (“yes2”).GetComponent
}
this.isEnterF = true;
}
}
void OnTriggerStay(Collider collider) {
if (Input.GetKeyDown (KeyCode.F)) {
Destroy (this.gameObject);
if (!isEnterF) {
GameObject.FindWithTag (“yes2”).GetComponent
}
this.isEnterF = true;
}
}
// Update is called once per frame
void Update () {
}
}
3.4 人物移动和加速跑
人物移动是使用transform方式进行设置的,而加速跑是监测键盘“shift”按键的输入,并增大人物的移动速度进行实现的,基本代码如下:
using UnityEngine;
using System.Collections;
public class Player : MonoBehaviour
{
private Animator anim;
private CharacterController controller;
private int cnt;
public AudioClip audioSource;
public float speed = 600.0f;
public float turnSpeed = 400.0f;
private Vector3 moveDirection = Vector3.zero;
public float gravity = 20.0f;
private AudioSource mAudioSource;
private Rigidbody rigidBody;
public float jumpSpeed = 40f;
void Start()
{
_mAudioSource = GetComponent
rigidBody = transform.GetComponent
rigidBody.useGravity = false;
rigidBody.isKinematic = true;
_mAudioSource.clip = audioSource;
controller = GetComponent
anim = gameObject.GetComponentInChildren
cnt = 0;
_mAudioSource.Play ();
}
void Update()
{
if (Input.GetKey (KeyCode.R)) {
gameObject.transform.position = new Vector3 (189.0631f, 250f, 371.857f);
return;
}
if ((Input.GetKey (“w”) || Input.GetKey (“s”)) && Input.GetKey (KeyCode.LeftShift)) {
speed = 50.0f;
if (mAudioSource.isPlaying == false && cnt % 60 == 0)
{
_mAudioSource.Play();
}
_ /_
anim.SetInteger (“AnimationPar”, 1);
cnt++;
} else if ((Input.GetKey (“w”) || Input.GetKey (“s”)) && !Input.GetKey (KeyCode.LeftShift)) {
speed = 30.0f;
anim.SetInteger (“AnimationPar”, 1);
cnt++;
} else
{
_mAudioSource.Stop();
anim.SetInteger(“AnimationPar”, 0);
}
if (controller.isGrounded)
{
moveDirection = transform.forward Input.GetAxis(“Vertical”) speed;
}
float turn = Input.GetAxis(“Horizontal”);
transform.Rotate(0, turn turnSpeed Time.deltaTime, 0);
rigidBody.MovePosition (moveDirection Time.deltaTime);
controller.Move(moveDirection Time.deltaTime);
if (Input.GetKeyDown (KeyCode.Space)) {
anim.SetInteger (“AnimationPar”, 1);
rigidBody.velocity += new Vector3 (0, 30, 0);
rigidBody.AddForce (Vector3.up jumpSpeed);
}
moveDirection.y -= gravity Time.deltaTime;
}
}
3.5 第三人称视角
第三人称相机的相关代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ThirdPersonCamera : MonoBehaviour
{
private const float Y_ANGLE_MIN = 0.0f;
private const float Y_ANGLE_MAX = 50.0f;
public Transform lookAt;
public Transform camTransform;
public float distance = 5.0f;
private float currentX = 0.0f;
private float currentY = 45.0f;
private float sensitivityX = 20.0f;
private float sensitivityY = 20.0f;
private void Start()
{
camTransform = transform;
}
private void Update()
{
currentX += Input.GetAxis(“Mouse X”);
currentY += Input.GetAxis(“Mouse Y”);
currentY = Mathf.Clamp(currentY, Y_ANGLE_MIN, Y_ANGLE_MAX);
}
private void LateUpdate()
{
Vector3 dir = new Vector3(0, 0, -distance);
Quaternion rotation = Quaternion.Euler(currentY, currentX, 0);
camTransform.position = lookAt.position + rotation * dir;
camTransform.LookAt(lookAt.position);
}
}
4 总结与收获
经过这学期Unity游戏引擎的学习,我对游戏开发有了大概的一个了解,了解到了与游戏开发有关的知识和岗位,为后续的发展提供了更多的可能性。
而这一次的Unity游戏引擎大作业,将前面所学的知识进行了一次融会贯通,并且在已学知识的基础上,进行举一反三,进一步发挥想象力去拓展创造。
另外,也加强了自己查阅资料和寻找资源的能力,在面对一些问题暂时不知道如何解决的时候,往往需要去网上搜索答案,经过这次大作业的锻炼,现在可以很快的定位到需要的资料文档,并对问题进行分析处理。而在游戏开发的过程中,游戏素材实际上是很重要的一部分,而在游戏素材缺失的情况下,可以在如Untiy游戏商店这样的在线素材商店进行下载,从而使游戏更加丰富多彩。
后续,也会继续学习相关的知识,并对这个小游戏进行进一步的完善,实现更多有趣好玩的功能~