序言
关于这篇教程的配套实例(游戏源码),我已经发到CSDN上了,等哪天通过了,你们就可以看到了。
这篇教程的很多内容,就是这个游戏的源码。
第一节
基本操作:
旋转:alt+鼠标左键。
平移:下压鼠标滑轮。
视角远近:鼠标滚轮,或alt+鼠标右键。
鼠标光标放到导航器上时,鼠标右键是视图菜单,可以选择合适的视角。
点导航器轴,轴会朝向观看者。
聚焦:选择一个物体,按F键,视角聚焦到该物体上。或层级面板里双击该物体。
摄像机视角与观察者视角一致:选中摄像机→游戏对象→对齐视图。
快捷键:移动w、旋转e、缩放r。
旋转时按ctrl键:15度的增量刻度旋转。
缩放:体积大小改变。也可以只沿一个方向的体积大小改变。
透视视图(perse):近大远小的立体视图。如果没有近大远小,就是正交视图(iso)。
导航器下面,persp改为iso就由透视视图变为正交视图。正交顶视图(点击正交后,再点导航器的y轴,变为顶视图)方便物体对齐。
ctrl和shift可多选,也可以鼠标框选。
复制:Ctrl+D,这里不是Ctrl+C,或在层级面板复制、粘贴。
输入框左边,鼠标拖动可以直接拉大小数值。
栅格一格是一个单位,也就是1米。
渲染:把模型的网格给予颜色和材质,从而显示出来。
物体设置颜色:新建材质,设置反射率(颜色),然后把材质文件拖动到物体上。
物体设置图片皮肤:网格渲染器→点击材质→图片拖动到反射率左边的方框里。
资源栏的物体可以拖动到场景里,拖动到层级面板里,也是同样的效果。
导入外部3D模型,格式一般要求为fbx。
物体的显示与不显示:右侧检查器面板里勾选。
注意:物体不显示之后,挂在该物体上的脚本也会失效,但是脚本中awake函数还会生效。
初始设置:编辑→首选项→外部工具→设置为visual studio,从而由visual studio编译。
创建c#脚本,文件名就是类名。脚本必须指定给一个物体,即便是空物体。
程序中this指当前物体对象。
第二节
Visual Studio的C#程序和unity界面的对应关系:
例如:在Visual Studio中,用C#语言设定了5个对象和变量。
public GameObject a;//物体对象
public Vector3 b;//物体三维坐标
public int i;//整数型变量
public string str;//字符串变量
public string[] c;//数组
public bool d;//布尔类型变量
把该脚本挂到一个物体上,点击该物体,在unity右边的检查器里,该脚本面板,就会出现这5个对象和变量的输入框,用于为其设定值。
一个脚本写好后,在层级面板拖动到一个物体上,那么这个物体就可以调用这个脚本。或者拖动到一个物体的检查器的空区域,也可以把该脚本附着到该物体上。
引用对象:
如果场景中有A、B两个物体,A物体用挂在自己身上的脚本很容易,但是A物体想控制B物体的组件,或者想调用挂在B物体上的脚本,这就需要引用B物体。方法是A物体脚本上定义一个组件对象,unity界面里就会出现该对象的输入框,把B物体拖动进去,就形成A物体对B物体的引用。
例如:public GameObject n;
注意:声明为public,unity界面才会显示该对象的输入框。
把B物体拖动到该输入框里,从而使A物体形成对B物体的引用。
然后A物体用B物体的组件,来控制B物体。
如果不用拖动进框方式,也可以用GameObject node = GameObject.Find("物体名称");//如果写成路径:父物体/子物体名称,这样可以避免重名
拖动进框方式的好处在于物体改名或换位置后,引用也会跟着改变,而find方式名字是固定的,不会跟着改变。
一个物体的脚本调用另一个物体的脚本里的函数:
public class dog : MonoBehaviour
{
public string n;
void Start()
{
}
void Update()
{
}
public void abc()
{
UnityEngine.Debug.Log("汪、汪、汪");//控制台显示变量值
}
public void ao()
{
UnityEngine.Debug.Log(n);
}
}
public class cat : MonoBehaviour
{
public GameObject node;//dog脚本所在的物体拖进此框
void Start()
{
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
dog g = node.GetComponent<dog>();
g.abc();
g.n = "嘎嘎";
g.ao();
}
}
}
函数的执行顺序:
先执行完所有c#文件的awake函数(如果设置了awake函数),再执行完所有的start函数,再执行所有的update函数,再执行所有的LateUpdate函数。update函数屏幕每刷新一次,都要执行一次,但start函数不再执行了,所以start函数只作为脚本的初始化用。
awake函数,比所有的start函数都要早执行,是最早执行的函数。默认不生成awake函数,需要手动设定。
update函数每次执行的时间间隔是不确定的。例如根据刷新频率的计算,理论上2.0毫秒执行一次,但实际中,有时候2.1毫秒执行一次,为什么?因为unity不能独占CPU,有时候该unity执行update函数了,可是CPU正被其它线程占用,要等一会(例如等0.1毫秒)才轮到update占用CPU。这就造成原本该2毫秒执行update函数,延迟到了2.1毫秒执行。这样的结果造成物体运动出现轻微的不匀速,例如理论上设定每update一次(2毫秒),物体走1米,但是这次延时了,2.3毫秒物体才走了1米,那么物体走的一会快,一会慢。为了让物体保持完美的匀速运动,update执行时间较长时,物体走的距离就该多一些,update执行时间较短时,物体就走的距离就该少一些。这就需要知道每次update执行的时间间隔,也就是delta,然后用物体每秒走的设定距离乘以delta。
Application.targetFrameRate = 60;就是使帧率尽量固定为60fps,约17毫秒更新一次,既17毫秒执行一次update函数。60fps是游戏流畅运行的最低帧率,低于这个帧率,游戏运行时,画面会出现卡顿。
如果一个值在awake、编辑器、start都设置了值,start方法决定最后值。当然如果该值在update函数里也设置了,那么屏幕一刷新,就是update决定的值。
LateUpdate函数最后执行,所以一般是给摄像机的,就是场景里各种方面都执行完了,最后再摄像机拍摄。
定时调用:
this.Invoke("abc", 1);//Invoke("要调用的函数名", 多久后调用),这里表示1秒后调用
this.InvokeRepeating("要调用的函数名",1,2);//1秒后调用,每隔2秒再执行一次
按键响应:
if (Input.GetKeyDown(KeyCode.Space))
{
UnityEngine.Debug.Log("空格");
}
if (Input.GetKeyDown(KeyCode.Return))
{
UnityEngine.Debug.Log("回车");
}
if (Input.GetKeyDown(KeyCode.Escape))
{
UnityEngine.Debug.Log("Esc");
}
Input.GetKey()长按,Input.GetKeyDown()单击。
鼠标左键的响应:Input.GetMouseButtonDown(0);
鼠标右键的响应:Input.GetMouseButtonDown(1);
鼠标中键(下压)的响应:Input.GetMouseButtonDown(2);
退出游戏:
编辑器界面:UnityEditor.EditorApplication.isPlaying = false;
游戏界面:Application.Quit();
第三节 坐标和角度
轴向:
x是物体的左右轴(默认指右),y是物体的上下轴(默认指上),z是物体的前后轴(默认指屏幕向里的前方)。
模型脸的方向,一般是自身坐标系的正z轴方向。
xyz轴的交点是轴心,和物体几何中心不同,可以把轴心设置在指定位置,而几何中心固定为物体形体结构的中心。
如果选两个物体,按轴心旋转是每个物体按照各自的轴心旋转,物体各转各的。而按几何中心旋转是物体按照整体的几何中心旋转,物体作为合体来转。
坐标:
A是父物体,B是子物体。A放在原点(x0,y0,z0)上。
A放在原点(x1,y0,z1)上。
可见:
如果一个物体作为在根节点,或根节点的父物体,例如A,position和LocalPosition没有区别。
如果一个物体作为子物体,例如B,position依然是全局坐标,是对于全局坐标0,0,0而言的位置偏移,所以是3,0,3。而局部坐标是对于其父物体而言的,所以不是3,0,3,而是2,0,2,就是以父物体作为坐标原点0,0,0。此外,界面Transform上显示的是2,0,2,所以界面上显示的是局部坐标LocalPosition,而不是position。所以使用一个三维软件时,先要搞清其界面上显示的到底是局部坐标,还是全局坐标。
Vector3 pos = this.transform.position;
Vector3 pos = this.transform.localPosition;
UnityEngine.Debug.Log(pos);//控制台显示变量值
注意:localPosition的l是小写。
显示一个坐标轴的值:
Vector3 pos = this.transform.position;
string n = pos.x.ToString();//x坐标值
角度:
可见:
A作为根节点,其全局角度(eulerAngles)和局部角度(localEulerAngles)一样,都是旋转了30度。
B作为A的子物体,设置上旋转60度,但是对于全局而言,已经旋转了90度。所以设置上旋转的60度,是相对于其父物体的角度而言的。所以从全局角度而言,B物体是在其父物体A已经旋转了30度的基础上,又旋转了60度。Transform界面的角度指局部角度(自身角度)。
Vector3 angle = this.transform.eulerAngles;
Vector3 angle = this.transform.localEulerAngles;
UnityEngine.Debug.Log(angle);
注意:eulerAngles的e是小写,localEulerAngles的l是小写。
B物体作为A物体的子物体,其实就是B拿A作为参照物,从而产生对A的相对运动。
再增加物体C。C不是A的子物体,也不是B的子物体,而是和A都在根节点上。
此脚本挂在物体C上。
public Transform ob;//参照物C
void Start()
{
Vector3 obpos = ob.transform.position;//获取C物体的坐标位置
obpos.x = obpos.x + 2;//这个坐标位置沿着x轴增加2
this.transform.position = obpos;//这个坐标轴的位置给B,作为B的位置
}
B不是C的子物体,所以即便参照C的位置,而移动到C的右边,但具体位置和方向都不符合要求。所以要让物体B移动到预期位置,最好让B成为C的子物体,就可以用C的坐标轴来规划物体B的位置,使物体B处于物体C的正右边。
public Transform zi;//子物体
void Start()
{
Vector3 pos = new Vector3(2,0,0);//创建一个x2,y0,z0的坐标位点
/*方法2:
Vector3 pos = this.transform.position;
pos.x = 2;
pos.y = 0;
pos.z = 0;
*/
zi.transform.localPosition = pos;//把pos这个坐标位点给子物体
}
图中蓝块是绿块的子物体。
那么子物体的x轴方向上的位置,localPosition(局部坐标)而言是2,而position(全局坐标)而言是3。
还要保持父物体和子物体的转角一致:
public Transform zi;//子物体
void Start()
{
Vector3 pos = new Vector3(2,0,0);//创建一个x2,y0,z0的坐标位点
/*方法2:
Vector3 pos = this.transform.position;
pos.x = 2;
pos.y = 0;
pos.z = 0;
*/
zi.transform.localPosition = pos;//把pos这个坐标位点给子物体
Vector3 pos2 = new Vector3(0,0,0);//0度转角
zi.transform.localEulerAngles = pos2;//把0度转角给子物体
}
子物体的这个转角(局部转角localEulerAngle)是相对于父物体的0度转角,但对于全局而言,物体已经转了30度,也就是说子物体的全局转角eulerAngles是30度。
测距:
该脚本挂在物体1上。
public Transform ob;//物体2
void Start()
{
//方法1
Vector3 p1 = this.transform.position;
Vector3 p2 = ob.transform.position;
Vector3 p = p2 - p1;
float i = p.magnitude;//两物体之间的位置差
//方法2
float j = Vector3.Distance(this.transform.position, ob.transform.position);
//显示
UnityEngine.Debug.Log(i);
UnityEngine.Debug.Log(j);
}
第四节 物体移动
移动方式1:
Vector3 pos = this.transform.localPosition;//局部坐标
pos.x = pos.x + 0.02f;
//注意:float值后面要写f
this.transform.localPosition = pos;
移动方式2:
float speed = 3;//每秒移动3米
float distance = speed * Time.deltaTime;//deltaTime是屏幕每次刷新而调用update函数的时间间隔(每次不一样)
Vector3 pos = this.transform.localPosition;
pos.x = pos.x + distance;
this.transform.localPosition = pos;
移动方式3:
float speed = 3;//每秒移动3米
float distance = speed * Time.deltaTime;//deltaTime是每帧的时间间隔(每次不一样)
//(x、y、z)填写负值就是倒着移动,也可以同时向两个或三个方向移动
//Space.World全局坐标系,Space.Self局部坐标系(沿着自身方向)
this.transform.Translate(0,0,distance,Space.World);
//this.transform.Translate()方式,比this.transform.localPosition方式更好
移动方式4:
Vector3 del = sp * Time.deltaTime;
//这里只剩2个参数,没x、y、z了,是因为x、y、z在unity界面里填
this.transform.Translate(del, Space.Self);
既然有填框,就需要定义一个Vector3的对象,且为public。
具体实例:按键控制物体移动。
把这个脚本拖动到一个物体上。
void Update()
{
if (Input.GetKey(KeyCode.W))//W键上移
{
float speed = 5;
//deltaTime是Update执行一帧(一次)所需的时间,每次不一定,如果执行时间较久,位移就该较长一些
float distance = speed * Time.deltaTime;
//transform是三维坐标,this是当前物体
//Translate(x,y,z,坐标系)用于设置物体的坐标位置,Space.Self是局部坐标系,Space.World是世界坐标系
this.transform.Translate(0, 0, distance, Space.Self);
}
if (Input.GetKey(KeyCode.S))//S键下移
{
float speed = 5;
float distance = speed * Time.deltaTime;
this.transform.Translate(0, 0, -distance, Space.Self);//+z上移,前移,-z下移,后移
}
if (Input.GetKey(KeyCode.D))//D键左移
{
float speed = 5;
float distance = speed * Time.deltaTime;
this.transform.Translate(distance, 0, 0, Space.Self);//+x左移
}
if (Input.GetKey(KeyCode.A))//A键右移
{
float speed = 5;
float distance = speed * Time.deltaTime;
this.transform.Translate(-distance, 0, 0, Space.Self);//-x右移
}
}
第五节 物体旋转
旋转方法1:
float Rotatespeed = 30;//每秒转30度角
transform.localEulerAngles = new Vector3(0, 0, 0);
Vector3 angles = this.transform.localEulerAngles;
angles.y = angles.y + Rotatespeed * Time.deltaTime;
this.transform.localEulerAngles = angles;
旋转方法2:
float Rotatespeed = 30;//每秒转30度角
this.transform.Rotate(0,Rotatespeed * Time.deltaTime,0,Space.Self);
实例:按键控制物体旋转。
把该脚本拖动到一个物体上。
if (Input.GetKey(KeyCode.E))//右转
{
//Rotate(x,y,z,坐标系),y轴设值,就是沿y轴旋转的角度。正值顺时针旋转,负值逆时针旋转
this.transform.Rotate(0, 0.5f, 0, Space.Self);
}
if (Input.GetKey(KeyCode.Q))//左转
{
this.transform.Rotate(0, -0.5f, 0, Space.Self);
}
公转(父物体旋转带动子物体旋转):
float RotateSpeed2 = 60;
Transform parent = this.transform.parent;//找到父物体
parent.Rotate(0, RotateSpeed2 * Time.deltaTime, 0,Space.Self);
第六节 向目标移动
物体A向目标物体B自动移动:
public GameObject flag;//目标。目标物体的y坐标必须是0,否则向目标移动的地面物体就逐渐飞起来了
bool ToObject = false;//是否自动移向目标
把B物体拖动到flag框里,形成对B物体的引用。
void Update()
{
//向目标自动移动
if (Input.GetKeyDown(KeyCode.Space))//空格键
{
ToObject = true;
}
if (ToObject == true)
{
MoveToObject();//向目标移动
}
}
//自动移向目标
public void MoveToObject()
{
//面向目标,flag是目标物体的对象,flag.transform是对象物体的坐标
this.transform.LookAt(flag.transform);
Vector3 p1 = this.transform.position;
Vector3 p2 = flag.transform.position;
Vector3 p = p2 - p1;
float distance_p = p.magnitude;//移动距离,就是两物体之间的位置差值
if (distance_p > 6)//两者距离还大于6时
{
float speed = 5;
float move = speed * Time.deltaTime;
this.transform.Translate(0, 0, move, Space.Self);
}
else//两者距离已经小于6时
{
ToObject = false;//不再移动
}
}
向量相加:
Vector3 a = new Vector3(1,2,3);
Vector3 b = new Vector3(4,5,6);
Vector3 c = a + b;
也就是x+x,y+y,z+z,结果是(5,7,9)。
magnitude(大小)表示向量长度,本身计算方法是根号下x平方加y平方加z平方。
第七节 物体跟随
物体跟随本身不用设置,因为子物体会自动跟随父物体,且会随着父物体一起转动。但是子物体物理碰触,而造成自身方向改变时,需要调整回原本的角度和位置。
public Transform zi;//子物体
void Update()
{
Vector3 pos = new Vector3(2,0,0);//创建一个x2,y0,z0的坐标位点
zi.transform.localPosition = pos;//把pos这个坐标位点给子物体
Vector3 pos2 = new Vector3(0,0,0);//0度转角
zi.transform.localEulerAngles = pos2;//把0度转角给子物体
}
第八节 物体显示和不显示
对于物体:
一个物体的不显示和恢复显示,只能由该物体的父物体来控制,因为如果把控制一个物体不显示和恢复显示的脚本,挂到该物体上,该物体不显示后,这个物体的脚本也就关闭了,那么就无法用该脚本恢复物体显示了。
既然是父物体控制子物体,就要在父物体定义GameObject对象,然后把子物体拖动到该框中,从而形成父物体对子物体的引用和控制。
实例:
public GameObject objshow;//加public才会显示,把子节点拖到该框里
void Update()
{
//物体的显示和不显示的脚本,只能放到父节点,因为如果子节点不显示了,子节点的脚本也就关闭了,那么false后true也就无效了
if (Input.GetKeyDown(KeyCode.F))
{
//objshow是子物体对象,SetActive(false)不显示物体,该物体和脚本都关闭
objshow.SetActive(false);
}
if (Input.GetKeyDown(KeyCode.G))
{
objshow.SetActive(true);//显示物体
}
}
对于canvas元素:
public Graphic targetGraphic;
public void cv()
{
if (targetGraphic != null)
{
targetGraphic.enabled = !targetGraphic.enabled;
}
}
其它:
随机显示数字:
int n = Random.Range(1, 10);//产生1到10之间的随机数
UnityEngine.Debug.Log(n);//控制台显示
第九节 文字输入和文字显示
创建文字对象:
先把canvas的渲染模式调为世界空间,然后就可以调整canvas大小了。
然后点击canvas下的文字节点,属性里Autosize,Min:1,这样就可以随着框而缩小文字了。
然后回到canvas把渲染模式改为屏幕空间-摄像机,然后渲染摄像机里选择具体的摄像机。
方法1:
using TMPro;
//TMP:Text Mash Pro
public TMP_Text tmpText;
void Start()
{
if (tmpText == null)
{
tmpText = GetComponent<TMP_Text>();//拖动进框方式的引用,可不写这一行
}
if (tmpText != null)
{
tmpText.text = "Hello, TextMeshPro!";
tmpText.color = Color.red;
tmpText.fontSize = 24;
}
}
拖动进框方式(自己引用自己),就三行:
using TMPro;
public TMP_Text tmpText;
tmpText.text = "Hello, TextMeshPro!";
方法2:
private TextMeshProUGUI textMeshPro;
private void Awake()
{
textMeshPro = GetComponent<TextMeshProUGUI>();
}
private void Start()
{
if (textMeshPro != null)
{
textMeshPro.text = "Hello, World";
}
}
输入与显示:
public TMP_InputField inputField;
public TMP_Text tmpText;
void Start()
{
inputField = GetComponent<TMP_InputField>();
inputField.onEndEdit.AddListener(OnInputEndEdit);//onEndEdit:输入完内容后按回车键
}
void OnInputEndEdit(string value)
{
tmpText.text = inputField.text;
}
第十节 图片
图片属性设置为sprite。
public Image image;//层级面板的节点中,canvas下的image拖进此框(图像框)
public Sprite sprite;//资源栏,设置sprite属性后的图片,拖进此框(精灵框)
void Start()
{
RectTransform rectTransform = image.rectTransform;
rectTransform.sizeDelta = new Vector2(744,425);//图片大小
rectTransform.anchoredPosition = new Vector2(0,0);//图片位置
//也可以不用设置,在界面里手动调整
}
void Update()
{
image.sprite = sprite;
}
图片、文字、输入框综合实例:
canvas显示模式:屏幕覆盖。
空对象挂此脚本:
using UnityEngine.UI;
using TMPro;
public Image image;//图片
public Sprite sprite;//精灵
public TMP_Text tmpText;//文字显示框
public TMP_InputField inputField;//输入框
void Start()
{
//文字显示框
if (tmpText == null)
{
tmpText = GetComponent<TMP_Text>();
}
if (tmpText != null)
{
tmpText.text = "文字测试。";
tmpText.color = Color.red;
tmpText.fontSize = 24;
}
//输入框
inputField = GetComponent<TMP_InputField>();
/*图片框位置
RectTransform rectTransform = image.rectTransform;
rectTransform.sizeDelta = new Vector2(74,42);//图片框大小
rectTransform.anchoredPosition = new Vector2(0,0);//图片框位置
文字框位置
RectTransform rectTransform2 = tmpText.rectTransform;
rectTransform2.sizeDelta = new Vector2(100, 30);//文字框大小
rectTransform2.anchoredPosition = new Vector2(100, 100);//文字框位置
*/
}
void Update()
{
image.sprite = sprite;//图片精灵
}
第十一节 音乐
播放音乐:
先添加Audio Source组件,把音乐拖进Audio Clip,关闭“唤醒时播放”(Awake Play)否则游戏一运行就播放音乐了
if (Input.GetKeyDown(KeyCode.M))
{
AudioSource audioSource = GetComponent<AudioSource>();//audioSource对象获取组件
audioSource.loop = true;//循环播放
if (audioSource.isPlaying)
{
audioSource.Stop();
}
else
{
audioSource.Play();
}
}
音乐盒:
public AudioClip[] songs;//歌单
//然后把音乐一个个的拖动到unity界面里,该数组变量框的左边位置
public int MusicNum = 0;//数组从0开始算第一个
void Update()
{
if (Input.GetKeyDown(KeyCode.M))
{
AudioClip clip = this.songs[MusicNum];//第几首歌
string MusicName = clip.name;//歌名
AudioSource ac = GetComponent<AudioSource>();
ac.clip = clip;
ac.Play();//播放歌曲
int len = songs.Length;//数组长度,就是总共有几首歌
if (MusicNum == len - 1)//当前是最后一首歌。MusicNum不能等于len,因为len是数组内容的个数,是从1开始计算的,而数组值的编号是从0开始计算的
{
MusicNum = 0;//还原为第一首歌
}
else
{
MusicNum = MusicNum + 1;//下一首歌
}
}
}
第十二节 摄像机
摄像机脚本:
//该脚本直接挂到摄像机上,摄像机位于根节点
public Transform target;//设置摄像机的目标(拖动进去)
public float caco_height = 1.5f;//摄像机高度
public float caco_distance = 2f;//摄像机距离
public float Damping = 2.5f;//速度
public float sensitivityMouse = 40f;//鼠标速度
float mX = 0.0f;//鼠标控制的旋转角度
float mY = 0.0f;//鼠标控制的旋转角度
void Start()
{
}
void Update()
{
if (Input.GetKeyDown(KeyCode.UpArrow))
{
caco_height = caco_height + 0.2f;
}
if (Input.GetKeyDown(KeyCode.DownArrow))
{
caco_height = caco_height - 0.2f;
}
if (Input.GetKeyDown(KeyCode.LeftArrow))
{
caco_distance = caco_distance - 0.2f;
}
if (Input.GetKeyDown(KeyCode.RightArrow))
{
caco_distance = caco_distance + 0.2f;
}
}
void LateUpdate()
{
//鼠标输入
mX += Input.GetAxis("Mouse X") * sensitivityMouse * 0.02f;
mY -= Input.GetAxis("Mouse Y") * sensitivityMouse * 0.02f;
//重新计算位置和角度
Quaternion mRotation = Quaternion.Euler(mY, mX, 0);
Vector3 mPosition = mRotation * new Vector3(0.0f, caco_height, -caco_distance) + target.position;
//设置相机的角度和位置
transform.rotation = Quaternion.Slerp(transform.rotation, mRotation, Time.deltaTime * Damping);
transform.position = Vector3.Lerp(transform.position, mPosition, Time.deltaTime * Damping);
}
消除锯齿:
窗口-包管理器:
该组件挂摄像机上。
Mode用SMAA,质量选择高。虽然FXAA最简单,对显卡要求也最低,但效果不如SMAA。
第十三节 物理系统:碰撞系统和重力系统
为了避免人物穿墙,也就是避免物体之间相互穿透,需要添加碰撞系统和重力系统。
人物添加:
碰撞系统:添加组件→physics→boxcollider
重力系统:添加组件→physics→rigidbody
包裹物体的绿框就是碰撞系统框,两个绿框(例如人物的绿框和房屋的绿框)碰在一起,就可以避免相互穿透。游戏运行时,是不显示的绿框的。
绿框大小是可以改变的,以适应物体。
不要一个绿框包住整个物体,那样不准确。应该给物体的每个组成部分,都包一个绿框。
对于预制体,在资源栏中,双击预制体,然后单独设置碰撞系统的绿框。
人物跑向墙,以一定的速度撞墙,人物会摔倒,因为有物理系统存在了。人物在空中,会落到地面上,但不会穿透地面。
rigidbody这个词里,rigid是刚体,就是指一个物体,运动中和受力后,形状不变,这样就可以使用牛顿力学三定律。与刚体相对而言的是流体,例如水,运动中和受力后,形状会改变。
碰撞框要做的稳定,否则人物撞墙容易摔倒。但碰撞体做的太大了,人走路容易碰到东西。
人物加box collider,要进的楼房加mesh collider。mesh collider是根据网格形状添加碰撞体,所以碰撞体比较贴合物体,但是mesh collider要给一栋楼房加碰撞体,就要给这个楼房里的每一个物件,都逐个加mesh collier,比较麻烦。对于不进的楼房,直接加个box collider框住就行了。
给人物加box collider,人物就不会穿墙了(墙加了box collider或mesh collider),但是人物加了mesh collider,人物依然会穿墙。
第十四节 触发器
Box Collider要勾选“是触发器”,才算是触发器。
触发器人物可以穿透。
关闭右边的Mesh Renderer,就不显示触发器墙了。
在触发器的物体上,挂此脚本:
void OnTriggerEnter(Collider other)//进入触发器
{
if (other.gameObject.name == "d1")//进入触发器的对象名称
{
UnityEngine.Debug.Log("进入触发器");
}
}
void OnTriggerStay(Collider other)//停留在触发器
{
if (other.gameObject.name == "d1")
{
UnityEngine.Debug.Log("停留在触发器");
}
}
void OnTriggerExit(Collider other)//离开触发器
{
if (other.gameObject.name == "d1")
{
UnityEngine.Debug.Log("离开触发器");
}
}
到了家门口,还要有触发器,负责开关门。
//开关门
public GameObject homedoor;//家门
public Transform girl;//子物体:女主角
public Transform home;//父物体:家里的一个物件作为父物体
public Transform boy;//父物体:男主角
void Start()
{
}
void Update()
{
}
void OnTriggerEnter(Collider other)//进入触发器
{
if (zong.weizhi == "家门口")//进入家门口的触发器
{
zong.weizhi = "进家";//全局变量(位置)设置
}
else if (zong.weizhi == "进家")//进入家里的触发器
{
//男主角和女主角分开(女主角离队)
girl.transform.SetParent(home);//设置父物体
girl.transform.localPosition = Vector3.zero;//初始化位置
girl.transform.localRotation = Quaternion.identity;//初始化角度
zong.weizhi = "家里";//全局变量(位置)设置
}
else if (zong.weizhi == "家里")//进入家里的触发器
{
zong.weizhi = "离家";//全局变量(位置)设置
}
else if (zong.weizhi == "离家")//进入家门口的触发器
{
//男主角和女主角一起(女主角入队)
girl.transform.SetParent(boy);//设置父物体
girl.transform.localPosition = Vector3.zero;//初始化位置
girl.transform.localRotation = Quaternion.identity;//初始化角度
zong.weizhi = "家门口";//全局变量(位置)设置
}
homedoor.SetActive(false);//开门:不显示门
this.Invoke("closedoor", 2);//2秒后调用程序:closedoor:关门
}
void closedoor()
{
homedoor.SetActive(true);//关门:显示门
}
zong.weizhi是另一个脚本,也就是总控脚本的变量:
public class zong : MonoBehaviour
{
//总控脚本
//全局变量,所有脚本共用
public static string weizhi;//男主角的位置
// Start is called before the first frame update
void Start()
{
//游戏开始时的初始化
weizhi = "家门口";//游戏开始时,男主角的位置
}
// Update is called once per frame
void Update()
{
}
}
第十五节 上楼梯和走路不摔倒
人物上楼梯会被楼梯台阶挡住,而无法上楼梯。unity上楼梯的方法,网上主要是射线法和烘培法,都太麻烦,我想了一些简单的方法:
上楼梯最简单的方法:铺板子。
unity的3D对象里有平面,调个合适大小,再旋转到合适角度,铺到楼梯上。板子不可见,但加碰撞体,楼梯可见,但不加碰撞体。这样上楼梯,实际就是上板子、上斜坡,这样就不会被楼梯台阶挡住了。
人物走路时踩的不稳、碰撞物体,和上斜坡时,都容易摔倒。
人物不摔倒:身体前后倾斜度大于15度时,就自动恢复身体直立。
if (this.transform.eulerAngles.x > 15)
{
this.transform.eulerAngles = new Vector3(0, this.transform.eulerAngles.y, 0);//身体摆正直立
}
此外,碰撞体做的稳定,以及增加阻力,也会减少摔倒的概率。
第十六节 跳跃与飞空
跳跃:
public float NormalGravity = -9.81f;//地球标准重力
public float ZeroGravity = 0;//零重力
if (Input.GetKey(KeyCode.W))//W键上移
{
Physics.gravity = new Vector3(0, ZeroGravity, 0);//y轴上的重力为0
float speed = 5;
float distance = speed * Time.deltaTime;
this.transform.Translate(0, distance, distance, Space.Self);
Physics.gravity = new Vector3(0, NormalGravity, 0);//y轴上的重力是地球正常重力
}
飞空:
该脚本挂在要移动的物体上。
public Transform ob;//目标物体
public float NormalGravity = -9.81f;//地球标准重力
public float ZeroGravity = 0;//零重力
bool arrive = false;//是否到了目标位置
void Start()
{
}
void Update()
{
if (Input.GetKey(KeyCode.Space) && arrive == false)
{
Physics.gravity = new Vector3(0, ZeroGravity, 0);//y轴上的重力为0
if (Vector3.Distance(this.transform.position, ob.transform.position) > 0.1f)//还没到目标位置
{
this.transform.position = Vector3.MoveTowards(this.transform.position, ob.transform.position, 2 * Time.deltaTime);//向目标移动
}
else//到了目标位置
{
arrive = true;//到了
Physics.gravity = new Vector3(0, NormalGravity, 0);//y轴上的重力是地球正常重力
}
}
}
第十七节 预制体
层级面板的节点拖动到资源面板中,该节点就成了预制体(prefab),也就是模板。
预制体拖动到层级面板中,预制体作为模板,拖动到层级面板里而产生的物体,就是预制体的实例(instance),和预制体完全一样。预制体改变了,实例也就随之改变。
脚本1:
public GameObject ob;//把预制体从资源栏直接拖动到该框中
public Transform kongob;//把空节点拖动到该框中,空节点是预制体实例产生的位置
void Start()
{
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
GameObject node = Instantiate(ob, null);//创建预制体的实例
//node.transform.position = Vector3.zero;//设置实例的具体位置,Vector3.zero表示(0,0,0)坐标
//node.transform.localEulerAngles = Vector3.zero;//设置实例的具体角度
node.transform.position = this.kongob.position;//新产生的预制体实例设置到指定空物体(空节点)位置上
}
}
脚本2:
//该脚本直接放到资源栏的预制体上
float lifetime = 15f;
// Start is called before the first frame update
void Start()
{
Invoke("dt", lifetime);
}
// Update is called once per frame
void Update()
{
float speed = 5;
float distance = speed * Time.deltaTime;
this.transform.Translate(0, 0, distance, Space.Self);
}
void dt()
{
Object.Destroy(this.gameObject);
}
实例物体的角度,随着另一个物体的角度:
定义一个transform对象,把另一个物体拖动进该框。
public Transform otherob;
node.transform.localEuleAngles = this.otherob.EuleAngles
或者写node.transform.rotation = this.otherob.rotation
第十八节 场景切换
第一步:点“生成设置”。
第二步:添加已打开场景。
第三步:点“生成”。
第四步:代码。
void OnTriggerEnter(Collider other)//进入触发器
{
if (other.gameObject.name == "d1")//进入触发器的对象名称
{
SceneManager.LoadScene("house");
}
}
第十九节 数据库
unity连接mysql数据库,并调用存储过程:
public TMP_InputField inputField;//输入框对象
public TMP_Text tmpText;//输出框对象
public string ShowText;//输出框的内容
void Start()
{
inputField = GetComponent<TMP_InputField>();//输入框获取组件
inputField.onEndEdit.AddListener(OnInputEndEdit);//输入完成后,对回车键的响应
}
void OnInputEndEdit(string value)
{
string shuru = inputField.text;//输入框的值
//为了数据库的安全,不可以输入的值有以下这些字符。此外,反斜杠是转义字符,所以这里用两个反斜杠\\表示一个反斜杠
if (shuru.Contains("'") || shuru.Contains('"') || shuru.Contains("-") || shuru.Contains("#") || shuru.Contains("/") || shuru.Contains("\\") || shuru.Contains("=") || shuru.Contains(">")
|| shuru.Contains("<") || shuru.Contains("%") || shuru.Contains("_") || shuru.Contains(";") || shuru.Contains("and") || shuru.Contains("or") || shuru.Contains("like")
|| shuru.Contains("insert") || shuru.Contains("set") || shuru.Contains("delete") || shuru.Contains("drop") || shuru.Contains("truncate"))
{
tmpText.text = "不规范输入";
}
else
{
//连接mysql数据库
//设置连接
MySqlConnection connection = new MySqlConnection();
connection.ConnectionString = "server=localhost;user=root;database=snow;port=3306;password=dream;charset=utf8";
MySqlCommand sqlCommand = new MySqlCommand("abc", connection);//存储过程的名称
sqlCommand.CommandType = System.Data.CommandType.StoredProcedure;
connection.Open();
//输入参数
MySqlParameter p1 = new MySqlParameter();
p1.ParameterName = "@say";
p1.Value = inputField.text;//输入框的值
//输出参数
MySqlParameter p2 = new MySqlParameter();
p2.ParameterName = "@reply";
p2.Direction = System.Data.ParameterDirection.Output;
//添加参数
sqlCommand.Parameters.Add(p1);
sqlCommand.Parameters.Add(p2);
//执行
sqlCommand.ExecuteScalar();
//关闭连接
connection.Close();
//显示
ShowText = (string)p2.Value;
tmpText.text = ShowText;
}
}
unity连接sqlite数据库:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;//添加1
using Mono.Data.Sqlite;//添加2
using System.IO;//添加3
public class sqlitecon : MonoBehaviour
{
public TMP_InputField inputField;//输入框对象
public TMP_Text tmpText;//输出框对象
string ShowText;//输出框的内容
// Start is called before the first frame update
void Start()
{
inputField.onEndEdit.AddListener(OnInputEndEdit);//输入完成后,对回车键的响应
}
// Update is called once per frame
void Update()
{
}
void OnInputEndEdit(string value)
{
string shuru = inputField.text;//输入框的值
//为了数据库的安全,不可以输入的值有以下这些字符。此外,反斜杠是转义字符,所以这里用两个反斜杠\\表示一个反斜杠
if (shuru.Contains("'") || shuru.Contains('"') || shuru.Contains("-") || shuru.Contains("#") || shuru.Contains("/") || shuru.Contains("\\") || shuru.Contains("=") || shuru.Contains(">")
|| shuru.Contains("<") || shuru.Contains("%") || shuru.Contains("_") || shuru.Contains(";") || shuru.Contains("and") || shuru.Contains("or") || shuru.Contains("like")
|| shuru.Contains("insert") || shuru.Contains("set") || shuru.Contains("delete") || shuru.Contains("drop") || shuru.Contains("truncate"))
{
tmpText.text = "不规范输入";
}
else
{
//第一步:sql指令
string sqlQuery = "SELECT score_col FROM abc WHERE name_col = '" + shuru + "'";//sql指令
//第二步:连接数据库
string connectionString = "Data Source=D://test.db;Version=3;";
SqliteConnection dbConnection;
dbConnection = new SqliteConnection(connectionString);
dbConnection.Open();
//第三步:执行指令
SqliteCommand dbCommand;
dbCommand = dbConnection.CreateCommand();
dbCommand.CommandText = sqlQuery;
dbCommand.ExecuteNonQuery();
//第四步:显示结果
SqliteDataReader dbReader;
dbReader = dbCommand.ExecuteReader();
while (dbReader.Read())
{
for (int i = 0; i < dbReader.FieldCount; i++)
{
//UnityEngine.Debug.Log(dbReader.GetName(i));//表格列名
//UnityEngine.Debug.Log(dbReader.GetValue(i));//表格列值
tmpText.text = dbReader.GetValue(i).ToString();
}
}
}
}
}