Unity入门------从简单2D驾驶游戏入手理解C#编程和Unity设计
接下来我们将以一个2D驾驶汽车送包裹的游戏项目为导向,进一步了解C#语言中的变量、if判断语句和方法等等。
传送门
Unity入门(一)——Unity的安装与相关环境配置
Unity入门(二)——Unity的初步认识与相关操作
Unity入门(三)——VS Code配置与C#脚本编写
首先是游戏机制设计,游戏机制设计是游戏设计中的核心,有的时候,好的游戏机制往往相比其他要素能带来更多的游戏性
先来看一下之后想要达成的游戏效果图
① 设计游戏所要实现的功能
玩家体验就是你想要玩家在游戏过程中所拥有的感觉
决定好游戏的设计是至关重要的,这决定了你的游戏完整性,如果没有一个好的游戏设计,很可能会导致之后的制作环节出现很大严重的割裂问题
首先从认识理解方法开始,先重建一个项目,构建最基础的游戏本体,实现低级功能
在Unity中创建一个新的2D项目
在项目中添加一个胶囊精灵代表之后的汽车元素
创建一个驾驶员C#脚本
将脚本添加至胶囊精灵
为胶囊精灵命名以便于分辨
定义
首先还是一样创建一个新的2D项目,在项目中创建一个胶囊元素,并新建一个C#脚本赋予胶囊元素
在本次初步尝试中,我们首先要实现胶囊元素的旋转功能。当我们选中该元素,调整其z轴上旋转字段的值时,我们可以看到胶囊元素的y轴正方向始终指向胶囊窄处正前方
接下来是代码实现,让我们进入VS Code对Driver.cs文件进行编码
该脚本文件涉及转换组件,因此我们需要调用转换类的方法;在对组件进行转换时,我们可以对其进行定位、旋转以及缩放的操作
首先我们来实现一个旋转45°角的操作:
代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Driver : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
transform.Rotate(0,0,45); // 调用转换类的旋转方法,并输入三个参数以实现z轴旋转的效果
}
// Update is called once per frame
void Update()
{
}
}
然后我们回到Unity窗口尝试运行
然后我们尝试将该代码放入Update方法中,使该胶囊元素每一帧都进行该动作
代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Driver : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
//transform.Rotate(0,0,45); // 调用转换类的旋转方法,并输入三个参数以实现z轴旋转的效果
}
// Update is called once per frame
void Update()
{
transform.Rotate(0,0,45); // 调用转换类的旋转方法,并输入三个参数以实现z轴旋转的效果
}
}
在游戏窗口,可以看到目前项目一秒有多少帧,该帧数取决于电脑配置:
运行效果如下:
如果你觉得对你来说太快了,那当然也可以调小每帧改变的z轴旋转值,比如0.1f(浮点数需要添加f以使编译器区分)
可以看到,此时的旋转速度就相对能够接受一些了。
前面我们实现了汽车原地转完,现在我们要模拟的是汽车在转弯过程中一边前进一边旋转的功能
尝试在Update中加入平移转换的操作,实现边缓慢旋转边向y轴正方向平移的操作,最终形成逆时针旋转的效果
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Driver : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
//transform.Rotate(0,0,45); // 调用转换类的旋转方法,并输入三个参数以实现z轴旋转的效果
}
// Update is called once per frame
void Update()
{
transform.Rotate(0,0,0.1f); // 调用转换类的旋转方法,并输入三个参数以实现z轴旋转的效果
transform.Translate(0,0.01f,0); // 调用转换类的平移方法,并输入三个参数以实现向y轴正方向移动的效果
}
}
效果如下:
接下来引入变量的概念,实现可变的转弯操作
对于有一定语言基础的同学来说这一部分就很熟悉了
基本变量类型
现在让我们将数字换为变量
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Driver : MonoBehaviour
{
float RotateSpeed = 0.1f; // 旋转速度
float AheadSpeed = 0.01f; // 前进速度
// Start is called before the first frame update
void Start()
{
//transform.Rotate(0,0,45); // 调用转换类的旋转方法,并输入三个参数以实现z轴旋转的效果
}
// Update is called once per frame
void Update()
{
transform.Rotate(0,0,RotateSpeed); // 调用转换类的旋转方法,并输入三个参数以实现z轴旋转的效果
transform.Translate(0,AheadSpeed,0); // 调用转换类的平移方法,并输入三个参数以实现向y轴正方向移动的效果
}
}
接下来可以对变量进行可控化处理,先将其转化为序列化字段,也就是给其添加一项属性,并在磁盘中分配一定的空间
[SerializeField] float RotateSpeed = 0.1f; // 旋转速度
[SerializeField] float AheadSpeed = 0.01f; // 前进速度
// 将变量通过赋予属性的方式进行序列化处理,即可以在检查器中访问它
// 当在检查器中更改该值后,该值将会被覆盖
在Unity路径"Project Settings-Input
Manager"中可以看到Unity自带的输入系统以及所定义的各变量:
其中第一个Horizontal和Vertical分别定义了使用键盘实现游戏元素左右和上下移动对应的键盘位置和各参数,而下面同样的二者则是针对游戏手柄的适配,这是这里最重要的几个变量。
然后我们可以在代码中更改变量,将之前所规定的固定大小的变量替换为输入系统中的变量:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Driver : MonoBehaviour
{
[SerializeField] float RotateSpeed = 0.1f; // 旋转速度
[SerializeField] float AheadSpeed = 0.01f; // 前进速度
// 将变量通过赋予属性的方式进行序列化处理,即可以在检查器中访问它
// 当在检查器中更改该值后,该值将会被覆盖
// Start is called before the first frame update
void Start()
{
//transform.Rotate(0,0,45); // 调用转换类的旋转方法,并输入三个参数以实现z轴旋转的效果
}
// Update is called once per frame
void Update()
{
float RotateAmount = Input.GetAxis("Horizontal"); // 每一帧需要改变的旋转量,通过获取Axis中的Horizontal变量赋值给自定义的变量
transform.Rotate(0,0,RotateAmount); // 调用转换类的旋转方法,并输入三个参数以实现z轴旋转的效果,其中每秒旋转的量为输入系统设置的
transform.Translate(0,AheadSpeed,0); // 调用转换类的平移方法,并输入三个参数以实现向y轴正方向移动的效果
}
}
此时由于坐标轴和左右键值与我们平时理解的相反,可以在变量RotateAmount前加一个负号
同理也可以对于游戏元素的上下变量进行更改,实现完全可控的游戏元素运动
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Driver : MonoBehaviour
{
[SerializeField] float RotateSpeed = 0.1f; // 旋转速度
[SerializeField] float AheadSpeed = 0.01f; // 前进速度
// 将变量通过赋予属性的方式进行序列化处理,即可以在检查器中访问它
// 当在检查器中更改该值后,该值将会被覆盖
// Start is called before the first frame update
void Start()
{
//transform.Rotate(0,0,45); // 调用转换类的旋转方法,并输入三个参数以实现z轴旋转的效果
}
// Update is called once per frame
void Update()
{
float RotateAmount = Input.GetAxis("Horizontal"); // 每一帧需要改变的旋转量,通过获取Axis中的Horizontal变量赋值给自定义的变量
float AheadAmount = Input.GetAxis("Vertical"); // 同理为前进量更改为输入系统变量
transform.Rotate(0,0,-RotateAmount); // 调用转换类的旋转方法,并输入三个参数以实现z轴旋转的效果,其中每秒旋转的量为输入系统设置的
transform.Translate(0,AheadAmount,0); // 调用转换类的平移方法,并输入三个参数以实现向y轴正方向移动的效果,其中每秒前进的量为输入系统设置的
}
}
实现的效果如下
当你觉得速度过快或过慢时均可在Input System中调整Sensitivity的值
也可以乘以自定义的变量(前面所用的固定变量),并调整自定义的变量值(更佳)
float RotateAmount = Input.GetAxis("Horizontal") * RotateSpeed;
float AheadAmount = Input.GetAxis("Vertical") * AheadSpeed;
示例:
在代码中将左右上下移动的行动乘以每帧所需的时间:
float RotateAmount = Input.GetAxis("Horizontal") * RotateSpeed * Time.deltaTime; // 每一帧需要改变的旋转量,通过获取Axis中的Horizontal变量赋值给自定义的变量,再乘上述规定的倍数和每帧所需的时间
float AheadAmount = Input.GetAxis("Vertical") * AheadSpeed * Time.deltaTime; // 同理为前进量更改为输入系统变量
由于每帧所需的时间很短,在Unity中运行会发现移动很慢。此时我们需要更改我们设定的转向和前进速度(我最终敲定为:200;20)
此时该项目已经实现了在任何硬件条件的电脑下的运行一致
首先在Unity项目中再创建一个精灵元素,形状可以自定
然后为各游戏元素添加"碰撞体"的属性
为汽车元素添加对应的Capsule
Collider属性,然后关闭元素显示,可以看到元素周围多了一圈表示碰撞体的绿色范围
同理给新添加的元素也添加碰撞体属性
接下来,我们还需给各个元素添加刚体属性:
刚体:Unity用来确定物体为一个实体,物理引擎系统能够理解的物体具有碰撞等的物理属性,产生物理交互
先给汽车元素添加刚体,可得效果(记得把重力比例Gravity scale设为0)
会发现,新添加的元素并没有因为碰撞而移动,此时需要为该三角元素也添加刚体属性
此时可以看到三角元素可以被汽车元素撞动了
接下来,我们需要为游戏元素编写C#脚本,使其在发生碰撞时将碰撞情况打印至控制台
在VS
Code,我们直接删除Start和update部分,转而使用另一个类似该两者的内置方法------OnCollisionEnter2D()方法
在新创建的C#脚本中编写如下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Collide : MonoBehaviour
{
private void OnCollisionEnter2D(Collision2D other) { // other:当发生碰撞时我们可以获得与碰撞相关的物体名等相关信息
Debug.Log("There is a collision happened"); // 将要输出的内容传递给控制台
}
}
要实现把内容打印到控制台(Console),我们还需要将该脚本作为组件添加给游戏元素
运行测试效果:
碰撞小结:
1. 至少需要有一个刚体,并需要为两者都添加碰撞体
2. 在碰撞事件发生时打印内容至控制台会有很大帮助
在本部分中我们将实现游戏对象越过触发器(Trigger)时,在控制台打印内容。
首先我们再次在项目中添加一个新的游戏对象,在为它添加对应的碰撞体。接下来我们思考一个问题,如何让我们的车辆元素驶过它而不是撞上它?
方法如下:
1. 在碰撞体变量中勾选(Is Trigger),表明该元素为触发器
此时运行项目,我们的车辆就可以穿过该元素了。可是我们可以发现我们的车辆元素始终在该元素的下层,这又该怎么办呢?
2.
我们之前提到过Unity像ps、pr一样,都有图层的概念,所以我们将汽车元素的图层调高一些就可以了
效果如下:
接下来我们要实现车辆元素经过触发器时在控制台输出内容的效果,打开我们的Collide的C#脚本,添加代码如下:
private void OnTriggerEnter2D(Collider2D other) {
Debug.Log("What was that?");
}
得到如下效果:
在本节中,我们将把现有的资产素材添加入我们的项目中,以完善我们的游戏项目。
所有用到的资产都可以评论或私信我免费给你
可以直接将资产文件夹拖入项目资产区
你可以直接将该文件夹内的素材拖入编辑区
前面我们已经提到过了在Unity中的所有精灵都是由像素组成的,而在Unity中的各种变量的单位都是unit
Unit并没有任何独立含义,它可以代表任何我们想让它代表的东西,如千米、米、英里等各种单位。当我们将资产拖入编辑区时,Unity会将其按照100像素/unit放置,所有资产调整大小必须要根据自己已经设定的含义根据Unit单位来进行,我们可以通过直接更改每一unit单位上的像素值来更改显示的资产的大小,这些资产我已经按我自己的想法更改了,当然你也可以更改成符合你预想的大小。
好的,现在我们已经有了基本的游戏元素,可是它们还是很杂乱无章,所以本节我们将依靠这些游戏元素打造一个较为完整的游戏地图。
首先我们先将我们的胶囊汽车元素更换成素材的里的汽车元素,可以直接对其更改所用的精灵元素:
同时,我们也需要对于其现在的形状在碰撞体上做一些更改,点击Edit Collider:
我们就获得了一些可以拖拽的碰撞体框架,将其拖拽到较为贴合即成功更新了该元素的碰撞体
主镜头的大小设置也是随心所欲、按需调整。这里需要提一下的是最好创建一个空游戏对象,并将其坐标置空,用来确定整个空间的正中心。然后可以将地图中的各种背景元素添加为该类子元素,便于管理。
接下来,设计属于你自己的地图吧:
在本节我们需要为我们的项目添加一个跟随摄像机,以便我们的汽车元素始终位于屏幕的正中间。
首先是创建一个C#脚本,命名为FollowCamera,在该代码中,我们需要为摄像机的位置创建一个车辆元素的引用
最好的引用方式是序列化一个游戏元素类型的变量代表所要引用的对象,如下编写:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 摄像机的位置需要和汽车元素的位置一致,一遍我们知道项目执行的内容
public class FollowCamera : MonoBehaviour
{
[SerializeField] GameObject ThingsToFollow; // 获取所要引用的游戏对象
// Update is called once per frame
void Update()
{
transform.position = ThingsToFollow.transform.position; // 将摄像机的位置设置与汽车元素一致
}
}
此时回到Unity,将该脚本赋予摄像机元素,我们先选择所要跟随的游戏元素:
试着运行游戏:
我们会惊讶地发现运行窗口中只有蓝色的背景,进入三维视角可以发现此时的问题所在
我们可以看到摄像机的z轴坐标也设为了和汽车元素一致,这样一来我们就无法看到汽车元素的全貌了。解决方法只需更改一行代码:
transform.position = ThingsToFollow.transform.position + new Vector3(0,0,-10); // 将摄像机的位置设置与汽车元素一致,并添加一个z轴方向的向量使摄像机相对汽车高度更高一些
此时回到Unity,我们已经可以实现镜头跟着汽车元素移动了。
但是此时的跟随还是存在一些延后的问题,因为汽车元素和镜头元素的更新会产生冲突,因此我们可以选择lateupdate方法来控制这一问题
LateUpdate:在所有Update函数调用后被调用,可用于调整脚本执行顺序。更改代码:
void LateUpdate()
我们需要制作的是一款驾驶网约车的游戏,因此我们需要添加乘客,并为其编写一个C#脚本命名为Delivery:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Delivery : MonoBehaviour
{
private void OnCollisionEnter2D(Collision2D other) { // other:当发生碰撞时我们可以获得与碰撞相关的物体名等相关信息
Debug.Log("There is a collision happened"); // 将要输出的内容传递给控制台
}
private void OnTriggerEnter2D(Collider2D other) {
Debug.Log("What was that?");
}
}
总体上和Collide差不多,接下来我们需要完善该脚本,首先先来了解一下if语句
当我们判断遇到的为乘客时,我们将会在控制台打印语句。在这里,我们判断游戏元素是否为乘客类时需要利用标签,以下为标签的介绍:
我们需要选中乘客元素并添加新的标签,再将该标签赋予乘客元素。再编写脚本代码如下:
public class Delivery : MonoBehaviour
{
private void OnCollisionEnter2D(Collision2D other) { // other:当发生碰撞时我们可以获得与碰撞相关的物体名等相关信息
Debug.Log("There is a collision happened"); // 将要输出的内容传递给控制台
}
private void OnTriggerEnter2D(Collider2D other) {
if (other.tag == "Passenger"){ // 判断所经过的元素是否属于乘客类
Debug.Log("Passenger is picked up!");
}
}
}
将该脚本赋予汽车元素,取代原来的Collide.cs的位置。运行效果如下:
当然,我们还需要添加一个目的地元素以及相应的控制台打印,在这里不多赘述,留作一个小任务给大家。
上述我们完成后,依然存在问题:当我们到达目的地的时候,无论是否有乘客都会打印提示,这肯定是不符合规定的。因此我们要引入布尔判断。
根据布尔变量更改后的代码为:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Delivery : MonoBehaviour
{
bool hasPassenger = false; // 用来判断车辆上是否有乘客的变量
private void OnCollisionEnter2D(Collision2D other) { // other:当发生碰撞时我们可以获得与碰撞相关的物体名等相关信息
Debug.Log("There is a collision happened"); // 将要输出的内容传递给控制台
}
private void OnTriggerEnter2D(Collider2D other) {
if (other.tag == "Passenger"){ // 判断所经过的元素是否属于乘客类
Debug.Log("Passenger is picked up!");
hasPassenger = true; // 当经过乘客时视为将乘客接上车
}
if (other.tag == "Destination" && hasPassenger){ // 判断所经过的元素是否属于目的地类以及车上是否有乘客
Debug.Log("The destination has arrived!");
hasPassenger = false; // 将乘客送到指定位置后乘客下车
}
}
}
效果如下:
车辆在没有乘客时不会输出提示语,只有当接上乘客后才可在经过目的地时打印文字
前面我们已经初步实现了网约车游戏的核心逻辑,现在就是要完善最终的系统。我们将规定:当车上有一名乘客时无法再接客,必须要将前一位乘客送至指定目的地后方可再次接客;当我们把乘客接上车时就将该乘客从地图上抹去。
我们将使用destroy方法来抹去游戏元素
首先更改代码,将destroy方法运用在判断乘客的if语句中
bool hasPassenger = false; // 用来判断车辆上是否有乘客的变量
if (other.tag == "Passenger"){ // 判断所经过的元素是否属于乘客类
Debug.Log("Passenger is picked up!");
hasPassenger = true; // 当经过乘客时视为将乘客接上车
Destroy(other.gameObject, DestroyDelay); // 经过乘客元素后在0.5秒后删除乘客元素
}
查看在Unity运行的效果:
接下来就是解决车辆一次只能接一位顾客的问题了,这个问题很容易从if语句解决:
if (other.tag == "Passenger" && !hasPassenger){ // 判断所经过的元素是否属于乘客类以及车上是否有乘客
和现实生活中的出租车一样,我们希望在接到一个乘客后更改提示颜色来显示车上是否有乘客,在这里我们选择直接更改车辆本身的颜色。
我们需要先在Delivery.cs代码中序列化一个Color32类型的颜色变量,通过更改RGB值实现颜色的改变。
[SerializeField] Color32 hasPassengerColor = new Color32 (241, 8, 69, 255); // 车上有乘客时的颜色
[SerializeField] Color32 noPassengerColor = new Color32(1, 1, 1, 1); // 当将乘客放下后转为空车色
接下来,我们还需要一个精灵渲染器的引用变量
SpriteRenderer spriteRenderer; // 精灵渲染器的引用
然后新建一个Start方法来获取初始的精灵渲染器设置,并在指定位置更改精灵渲染器的设置
private void Start() {
spriteRenderer = GetComponent<SpriteRenderer>(); // 获得初始的精灵渲染器状态
}
private void OnCollisionEnter2D(Collision2D other) { // other:当发生碰撞时我们可以获得与碰撞相关的物体名等相关信息
Debug.Log("There is a collision happened"); // 将要输出的内容传递给控制台
}
private void OnTriggerEnter2D(Collider2D other) {
if (other.tag == "Passenger" && !hasPassenger){ // 判断所经过的元素是否属于乘客类以及车上是否有乘客
Debug.Log("Passenger is picked up!");
hasPassenger = true; // 当经过乘客时视为将乘客接上车
spriteRenderer.color = hasPassengerColor; // 将颜色变为车上有客的颜色
Destroy(other.gameObject, DestroyDelay); // 经过乘客元素后在0.5秒后删除乘客元素
}
if (other.tag == "Destination" && hasPassenger){ // 判断所经过的元素是否属于目的地类以及车上是否有乘客
Debug.Log("The destination has arrived!");
hasPassenger = false; // 将乘客送到指定位置后乘客下车
spriteRenderer.color = noPassengerColor; // 将颜色变为空车的颜色
}
}
记得在Unity将不透明度调成最大值,最终效果:
该节我们将为游戏添加加速道具和碰撞减速机制。
先在编辑区加一个(或几个)加速道具图标,然后让我们回到Driver.cs脚本
创建缓速速度和加速速度变量:
[SerializeField] float slowSpeed = 15f; // 缓速速度
[SerializeField] float boostSpeed = 30f; //加速速度
接下来,就要为我们的项目添加加速减速机制,可以稍微考察自己一下,停止不往下翻,看看自己是否可以独立完成这两个机制。
好的,现在我来公布一下代码,看看你是否做对了,任何不懂的地方都在注释中写了,希望对你有帮助。
private void OnCollisionEnter2D(Collision2D other) { // 当碰撞时执行以下代码块
Debug.Log("You have to calm yourself down.");
AheadSpeed = slowSpeed; // 将速度改为缓速速度
}
private void OnTriggerEnter2D(Collider2D other) {
if (other.tag == "speedup" ){ // 判断所经过的元素是否属于加速器类
Debug.Log("Speed up!");
AheadSpeed = boostSpeed; // 将速度改为加速的速度
}
}
最终效果:
好了,本次内容大概就到这里,从一个简单的2D游戏项目入手大致介绍了游戏设计的流程,并对C#初级编程语言有了一定的了解。如果你有任何问题不懂或者需要代码的,欢迎评论或私信,我将及时回复。后续有可能的话我也会按照自己的想法继续完善这个项目,并且将所有素材程序等放出来。接下来,我们将会进一步深入C#编程以及Unity高级操作,敬请期待。。。
Unity start说明文档:Unity - Scripting API: MonoBehaviour.Start()
(unity3d.com)
Unity 变换旋转说明文档:Unity - Scripting API: Transform.Rotate
(unity3d.com)
Unity update说明文档:Unity - Scripting API: MonoBehaviour.Update()
(unity3d.com)