本文首先补充了一些Unity3D的知识,然后比较详细介绍了如何配置Unity3D的C#编程环境,最后给出了几个Unity3D制作小游戏的例子。这三个Example都基于同一个场景,是逐渐完善的过程。
①Unity脚本可以用很多编辑器来编辑,使用VS的话,由于VS很强大,开发会比较方便,断点调试等功能也可以依靠UnityVS插件实现。
②在Scene中,键盘的上下左右也可以移动视角。
③Inspector中的每个组件对应一个脚本。
④多个添加了Collider的物体,如果把它们叠在一起,只要一运行就会弹开来(下图都添加了Box Collider)。
⑤空对象:默认只有个Transform组件,别的得自己添加。
⑥添加自定义组件:有两种办法
第一种:直接在Project面板右键创建C# Script,然后把该Script拖到Hierachy面板中需要添加该组件的物体上去。
第二种:首先在Hierachy面板中选择需要添加组件的对象,然后在Inspector面板中,点击Add Component,选择New script即可添加。
⑦Input
C#脚本中会大量使用Input对象获取键盘和鼠标输入,Input到底提供了什么信息,可以在如下位置查看:
⑧一个物体的collider勾选成为触发器,就失去所有碰撞物理了,需要我们手动代码处理。
对于新手来说,这东西真是愁死我了,一直有问题,经过很多尝试和搜索资料后才最终解决了。之前文章中介绍的方法大体也是没问题的,这里只有一些细节变化。
①Visual Studio安装。
首先下载安装最新版本的Visual Studio,在Installer中要勾选Unity3D游戏开发一项【我并没有下载最新版本,而是用的以前安装的vs2017】:
顺带着也可以勾选一些别的需要的功能【我还安装了.Net桌面开发、C++桌面开发、通用Windows平台开发、.NET Core几项】。然后就是漫长的等待,直到安装完毕。【我之所以遇到问题,就是因为我安装的vs2017版本太老了,在installer中点击更新后,就解决了在unity中无法使用vs的问题。】
②Unity3D安装。
点击这里进入下载页面。选择<回头用户>选项,会进入下载页面:
在下载页面勾选确认checkbox,点击Download Unity Hub即可,下好后傻瓜式安装。
打开Unity Hub,在如下位置点击安装,选择最新版本安装,然后等待安装完成,可以勾选中文语言包【我习惯英文就不勾选了】,如图所示我安装的是Unity 2019.3.2f1版本。注意,点击安装后,这个Hub的反应极慢,很可能还会显示<没有Unity版本>这样的页面,多等待一会儿应该就能解决。
安装的过程也很缓慢,等待即可。
③关联vs2017到Unity3D。
首先,在Hub中随便创建一个工程。
然后在Project面板中,右键->Import Package->Custom Package:
会弹出资源管理器,我们在如下位置找到资源包文件:
点击打开即可。此时在Project面板的Assets目录下会出现UnityVS的文件夹。
然后,我们点击菜单栏的Edit->Preferences
在弹出的面板中,在下图中的位置选择vs2017作为编辑器即可:
此后,我们如果在unity中创建了C#脚本,双击即自动打开vs2017,观察是否报错,没报错基本就可以了。这时候在vs2017中编写代码,发现自动补全/智能提示功能正常。
这个例子在之前文章所使用的视频教程中也有类似的,在这里系统整理下。Unity的一些基本知识参考之前三篇文章,跟脚本无关的内容这里不展开。
本例子的目的是构建一个场景,场景中有一个立方体,使用键盘wasd可以控制立方体移动,使用shift可以加速移动,使用space跳起,使用鼠标可以调整俯仰视角,调整立方体的朝向。
首先,创建一个3D项目。然后删除掉Camera对象,只保留Directional Light。
然后,添加我们需要的对象。需要的游戏对象有:一个Terrain,一个Empty,然后给Empty创建两个子对象Cube和Camera。我把Empty的名字改成了MoveObject。如下图所示:
然后,我们分别修改这四个物体的组件。
(1)对于Terrain,我们不需要修改,使用默认的参数,为了确保和读者统一,特别说明下Transform组件参数和Terrain组件中的尺寸信息。
可以修改下地形的材质。
(2)对于MoveObject,添加一个Character Controller,一个Box Collider组件,一个自定义空白组件CubeMove。
调整下Transform组件:
调整一下BoxCollider组件:
调整一下Character Controller的Height、Radius、Y参数,使之恰好包围Cube:
(3)对于Cube,先删掉其Box Collider组件,然后需要对其位置进行一定调整,也就是调整Transform组件。
(4)对于Camera,则添加一个名为CubeViewRotate的自定义空白组件。并调整Transform到Cube顶部的中心位置。
接下来我们编写脚本的内容。
①在Project面板中,双击CubeMove文件,如果配置一切正常,会自动打开VS,且VS的解决方案管理器中不是空的,有当前的Unity项目。编写如下代码(只要会编程应该就能大概看懂):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeMove : MonoBehaviour
{
// Update is called once per frame
public float speed; // 速度,外部赋值
public CharacterController cubeController; // 移动控制器,外部赋值
public float cubeJumpSpeed = 5; // 跳起时速度,外部可修改
private const float g = 10; // 重力加速度
private Vector3 cubeMoveVec; // 计算移动量,注意 transform.right 和 transform.forward 是 Vector3
// 每帧都会调用 Update 函数
void Update()
{
// 获取键盘移动操作输入
float x = 0, z = 0;
if (cubeController.isGrounded) // 跳起来就不允许控制了
{
x = Input.GetAxis("Horizontal"); // 获取水平方向两个控制量
z = Input.GetAxis("Vertical");
// 合成控制量,并转换到local坐标系
cubeMoveVec = (transform.right * x + transform.forward * z) * speed;
if (Input.GetKey(KeyCode.LeftShift))// 如果按下了左侧Shift则跑动
{
cubeMoveVec *= 2.5f;
}
if (1 == Input.GetAxis("Jump")) // 如果按下了Space则跳起来,设置cubeJumpSpeed的跳起速度
{
cubeMoveVec.y = cubeJumpSpeed;
}
}
else // 如果不在地面,则逐渐让物体降下来
{
cubeMoveVec.y -= g * Time.deltaTime;
}
// 执行移动命令
cubeController.Move(cubeMoveVec * Time.deltaTime);
}
}
然后保存,回到Unity页面,发现MoveObject的CubeMove出现了我们在脚本中定义的三个public类型变量,我们设置速度为10,跳起速度为5,然后把Hierarchy中的MoveObject拖到Cube Controller变量上。
②对于CubeViewRotate脚本,我们则填写如下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeViewRotate : MonoBehaviour
{
public float cubeViewRotateSpeed;
public Transform cubeTransform;
private float cubeUpAngle;
// Start is called before the first frame update
void Start()
{
// 锁定鼠标位置
Cursor.lockState = CursorLockMode.Locked;
}
// Update is called once per frame
void Update()
{
// 获取鼠标偏移量,并限制纵向角度范围
float x, y;
x = Input.GetAxis("Mouse X") * cubeViewRotateSpeed * Time.deltaTime;
y = Input.GetAxis("Mouse Y") * cubeViewRotateSpeed * Time.deltaTime;
cubeUpAngle -= y;
cubeUpAngle = Mathf.Clamp(cubeUpAngle, -80, 80);
// 像机偏转 - 垂直方向
this.transform.localRotation = Quaternion.Euler(cubeUpAngle, 0, 0);
// MoveObject旋转 - 水平方向
cubeTransform.Rotate(Vector3.up * x);
}
}
同样拖动MoveObject到Cube Transform上,且设置角速度为100。
在上个基础上,添加了可移动目标以及射击功能。
需要添加四个新的游戏对象。
(1)枪Gun
给Camera创建名为Gun的子空对象。添加Sprite Renderer和Audio Source组件,以及名为CubeGunShoot的自定义组件。
Sprite Renderer是枪的准星,按照下图操作选择准星。
Audio Source是枪声,我们去网上下载一个枪声的mp3文件,放到project中,并按下图拖到组件中,然后把Play On Awake勾掉:
(2)子弹Bullet
创建完成后,把它拖到Project中成为预制体,并删掉Hierarchy中的对象。
创建名为Bullet的Sphere,添加Rigidbody组件和名为CubeBulletMove的自定义组件。
修改Transform:
修改Rigidbody:
(3)子弹碎片 BulletFragment
创建完成后,把它拖到Project中成为预制体,并删掉Hierarchy中的对象。
创建名为BulletFragment的空对象,添加名为CubeBulletFragmentDisappear的自定义组件。
创建五个BulletFragment的Cube类型子对象,每个Cube进行同样的设置。
首先都添加Rigidbody。
都修改Transform:
都修改Rigidbody:
(4)敌人Enemy
创建完成后,把它拖到Project中成为预制体,并删掉Hierarchy中的对象。
创建一个名为Enemy的Cube类型对象,添加Rigidbody,Character Controller和自定义Cube Enemy。
添加一个名为Enemy的tag:
修改Transform:
修改Box Collider:
修改Rigidbody:
此外,可以添加子物体以区分正反面,添加材质修改外观。
(5)给MoveObject添加标签:
(6)Terrain对象添加脚本
Terrain添加一个Cube Terrain的自定义组件。
(7)Hierarchy面板和预制体
以上修改完成后,Hierarchy面板的样子:
我把Project中的预制体放到了一个文件夹中:
①修改CubeMove
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeMove : MonoBehaviour
{
// Update is called once per frame
public float speed; // 速度,外部赋值
public CharacterController cubeController; // 移动控制器,外部赋值
public float cubeJumpSpeed = 5; // 跳起时速度,外部可修改
public int cubeHp = 100; // 自身血量
private const float g = 10; // 重力加速度
private Vector3 cubeMoveVec; // 计算移动量,注意 transform.right 和 transform.forward 是 Vector3
// 每帧都会调用 Update 函数
void Update()
{
// 如果hp已经没了,那么就死掉吧
if (cubeHp <= 0)
{
Destroy(gameObject);
}
// 获取键盘移动操作输入
float x = 0, z = 0;
if (cubeController.isGrounded) // 跳起来就不允许控制了
{
x = Input.GetAxis("Horizontal"); // 获取水平方向两个控制量
z = Input.GetAxis("Vertical");
// 合成控制量,并转换到local坐标系
cubeMoveVec = (transform.right * x + transform.forward * z) * speed;
if (Input.GetKey(KeyCode.LeftShift))// 如果按下了左侧Shift则跑动
{
cubeMoveVec *= 2.5f;
}
if (1 == Input.GetAxis("Jump")) // 如果按下了Space则跳起来,设置cubeJumpSpeed的跳起速度
{
cubeMoveVec.y = cubeJumpSpeed;
}
}
else // 如果不在地面,则逐渐让物体降下来
{
cubeMoveVec.y -= g * Time.deltaTime;
}
// 执行移动命令
cubeController.Move(cubeMoveVec * Time.deltaTime);
}
public void beAttacked(int hpHurt) // 被攻击到掉血
{
cubeHp -= hpHurt;
}
}
此后会多出Cube Hp的可设置属性,这是我们自己的血量:
②CubeBulletFragmentDisappear脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeBulletFragmentDisappear : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Destroy(gameObject, 0.3f); // 0.3秒后自动消失
}
}
③CubeBulletMove脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeBulletMove : MonoBehaviour
{
// Start is called before the first frame update
private const float bulletSpeed = 200; // 子弹速度
private const float bulletLiveTime = 3; // 子弹存活时间
public Rigidbody bulletRigid; // 给子弹初始一个力,子弹是刚体,此后会自动掉落
public GameObject bulletFragment; // 子弹碎片
void Start()
{
Destroy(gameObject, bulletLiveTime); // bulletLiveTime后销毁
bulletRigid.velocity = (this.transform.forward) * bulletSpeed; // 子弹初始受力
}
private void OnCollisionEnter(Collision collision) // 击中敌人,对其造成伤害
{
if ("Enemy" == collision.gameObject.tag)
{
collision.gameObject.GetComponent<CubeEnemy>().beAttacked(20);
}
if ("MoveObject" != collision.gameObject.tag) // 不允许子弹击中自己
{
GameObject bulletFragmentObj = GameObject.Instantiate(bulletFragment, transform.position, transform.rotation);
Destroy(gameObject);
}
}
}
Inspector显示(点击Project中Bullet预设体)如下,按箭头拖拽给变量赋值:
④CubeEnemy脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeEnemy : MonoBehaviour
{
public int enemyHp = 100; // 敌人血量
public float enemyTurnInterval = 10; // 未发现自己时,敌人转弯周期
public CharacterController enemyController; // 敌人移动控制器
private float enemyLastTurnTime = 0; // 上次转弯时刻
private float enemyMoveAngle = 0; // 运动角度
private const float enemySpeed = 5; // 敌人移动速度
Vector3 dirVec; // 自己与敌人相对位置
Vector3 enemyMoveVec; // 敌人移动方向
public Rigidbody enemyRigid; // 敌人刚体
private void Start() // 初始化
{
enemyLastTurnTime = -100;
enemyMoveVec = transform.forward * Mathf.Sin(enemyMoveAngle) + transform.right * Mathf.Cos(enemyMoveAngle);
}
// Update is called once per frame
void Update()
{
if (enemyHp <= 0) Destroy(gameObject); // 没血就销毁
// 以下一段控制如果敌人发现与自己距离达到一定范围,就去攻击自己,否则就随机乱走
float dis = 100000000;
GameObject cubeObject = GameObject.FindGameObjectWithTag("MoveObject"); // 获取自己的引用
if (cubeObject != null) // 计算相对方位
{
dirVec = cubeObject.transform.position - transform.position;
dirVec.y = 0;
dis = Vector3.Distance(cubeObject.transform.position, transform.position);
}
if (dis < 50)
{
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dirVec), 0.3f);
transform.position += dirVec.normalized * enemySpeed * Time.deltaTime * 2;
//enemyRigid.velocity = dirVec.normalized * enemySpeed;
}
else if (dis < 200)
{
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dirVec), 0.3f);
transform.position += dirVec.normalized * enemySpeed * Time.deltaTime * 0.5f;
//enemyRigid.velocity = dirVec.normalized * enemySpeed * 2;
}
else // 随机移动
{
if (enemyTurnInterval <= (Time.time - enemyLastTurnTime))
{
enemyLastTurnTime = Time.time;
enemyMoveAngle = Random.value * Mathf.PI;
enemyMoveVec = transform.forward * Mathf.Sin(enemyMoveAngle) + transform.right * Mathf.Cos(enemyMoveAngle);
}
// print(transform.position);
if (transform.position.x > 950 || transform.position.x < 50 || transform.position.z > 950 || transform.position.z < 50)
{
// 防止出界
enemyMoveVec = transform.position;
enemyMoveVec.x = 500 - enemyMoveVec.x;
enemyMoveVec.z = 500 - enemyMoveVec.z;
enemyMoveVec.y = 0;
}
transform.position += enemyMoveVec.normalized * enemySpeed * Time.deltaTime;
// enemyRigid.velocity = enemyMoveVec.normalized * enemySpeed;
}
}
private void OnTriggerEnter(Collider other) // 击中自己时,扣除自己血量
{
// print(collision.gameObject.tag);
if ("MoveObject" == other.gameObject.tag)
{
// print(collision.gameObject.tag);
other.gameObject.GetComponent<CubeMove>().beAttacked(10);
}
}
public void beAttacked(int hpHurt) // 被击中时的反应
{
enemyHp -= hpHurt;
}
}
⑤CubeGunShoot脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeGunShoot : MonoBehaviour
{
public GameObject bullet; // 子弹对象
public int cubeGunShootInterval = 10; // 射击间隔
private int cubeGunShootCnt = 0; // 射击间隔辅助
public AudioSource cubeGunShootAs; // 射击音效
// Update is called once per frame
void Update()
{
if (1 == Input.GetAxis("Fire1") && 0 >= cubeGunShootCnt)
{
// 射击
GameObject bulletObj = GameObject.Instantiate(bullet, transform.position + Vector3.forward * 0.1f, transform.rotation);
cubeGunShootCnt = cubeGunShootInterval;
cubeGunShootAs.Play();
}
cubeGunShootCnt--;
}
}
Inspector面板:
⑥CubeTerrain脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeTerrain : MonoBehaviour
{
// Start is called before the first frame update
public GameObject cubeEnemy; // 敌人对象
public int cubeEnemyNum = 1000; // 敌人数量
void Start()
{
// 随机创建敌人对象
for (int i = cubeEnemyNum; i > 0; i--)
{
Vector3 cubeEnemyObjPos;
cubeEnemyObjPos.x = Random.Range(200f, 800f);
cubeEnemyObjPos.y = Random.Range(1.6f, 1.7f);
cubeEnemyObjPos.z = Random.Range(200f, 800f);
Vector3 cubeEnemyObjAng;
cubeEnemyObjAng.x = 0f;
cubeEnemyObjAng.y = Random.Range(0f, Mathf.PI);
cubeEnemyObjAng.z = 0f;
GameObject cubeEnemyObj = GameObject.Instantiate(cubeEnemy, cubeEnemyObjPos, Quaternion.Euler(cubeEnemyObjAng));
}
}
// Update is called once per frame
void Update()
{
}
}
Inspector面板:
这个部分的内容是非常非常认识性质的,聊胜于无吧。
面板是随着Camera一起动的,会随着窗口的变化而变化。这里我们用面板上的准星替换掉Camera上的准星。
首先,在Hierarchy中找到Gun, 然后把其Sprite Renderer禁掉:
然后,我们在Hierarchy中创建一个UI/Canvas,然后在Scene中按F键,发现多出了一个面板组件。
最后,我们给Canvas创建一个UI/Image子对象,并在其Image组件中的Source Image选项中选择准星:
这里仍然搞个简单的例子,为我们的Cube建个家,带有可以开闭的门。
首先创建一个空对象,命名为Home,然后给它创建四个子Cube对象,并搭成一个房子形状,再创建Home的两个Sphere子对象,作为开门的按钮:
两个Sphere要勾选触发器:
然后,我们把子对象Cube(1)作为门,给Cube(1)创建一个Animator组件。
在Project面板中,创建两个动画,分别命名为DoorOpen和DoorClose:
然后把DoorClose拖到Cube(1)的Inspector面板,会自动和刚刚为Cube(1)创建的Animator组件绑定,同时在Project中会出现一个动画控制器。然后双击打开控制器:
把Project中的DoorOpen也拖到面板中,然后把三个元素连成上面图中的样子,连接方法是:右键箭头出发的元素,然后选择Make Transition,然后再选择被连接的组件即可。
双击DoorOpen,会进入动画编辑器,进入后我们点击Cube(1)对象,然后点击控制器中红色圆圈录制动画,把时间线拖动到1s的位置,然后在Scene中把Cube(1)给拉起高度,然后再次点击红色圆圈,动画制作完毕,具体地:
①打开控制器,点击Cube(1),开始录制动画,时间拨到1s的操作顺序为:
②拉起门,结束录制:
然后,我们重新进入动画控制器,并进入Parameters面板,添加Bool变量opendoor:
然后分别点击DoorClose指向DoorOpen的箭头和DoorOpen指向DoorClose的箭头,设置对变量opendoor的绑定分别为true和false,这样,我们只要在人物碰到Sphere按钮时,修改这个opendoor变量的值,就可以控制动画的流程了。
最后,我们编写名为CubeDoorOpen的脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeDoorOpen : MonoBehaviour
{
public GameObject door;
private void OnTriggerEnter(Collider other)
{
if ("MoveObject" == other.gameObject.tag)
{
bool x = door.GetComponent<Animator>().GetBool("opendoor");
x = !x;
door.GetComponent<Animator>().SetBool("opendoor", x);
}
}
}
把它添加给Home下的两个Sphere,并把Cube(1)拖给变量door:
效果为:
项目已经上传Github,链接。
这哥们的视频我是在上个教程的评论区发现的,先看看 ↩︎