Bootstrap

unity教程(定期更新)

序言

关于这篇教程的配套实例(游戏源码),我已经发到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();

                }

            }

        }

    }

}

;