Unity 环境搭建

Unity 引擎是什么

游戏引擎是什么?

游戏引擎是指一些已编写好的可编辑电脑游戏系统或者一些交互式实时图像应用程序的核心组件。这些系统为游戏设计者提供各种编写游戏所需的各种工具,其目的在于让游戏设计者能容易和快速地做出游戏程序而不用由零开始。

通俗来讲:

游戏引擎就是专门做游戏的软件。它提供了很多现成的功能供我们使用让开发游戏事半功倍

游戏引擎对于我们的意义

  1. 降低做游戏的门槛……不用学太多的专业知识
  2. 提升游戏开发的效率……不用从零开始

如何学习 Unity

Unity 相当于就是一个游戏开发工具包,我们只要学会使用工具包中的各个工具即可而使用这些工具的媒介就是我们已经学习完毕的 C#程序语言

总结

  1. 什么是游戏引擎——开发游戏的软件
  2. 游戏引擎对于我们的意义——门槛低,效率高
  3. 如何学习游戏引擎——软件操作、公共 API、核心系统

Unity 就是一个做游戏的工具软件,它可以让开发游戏事半功倍。我们将通过我们学习的 C#程序语言,作为和这个工具沟通的媒介,制作出最终的游戏产品。

新建工程和工程文件夹

工程文件夹

  1. Assets:工程资源文件夹(美术资源,脚本等等)
  2. Library:库文件夹(Unity 自动生成管理)
  3. Logs:日志文件夹,记录特殊信息(Unity 自动生成管理)
  4. obj:编译产生中间文件(Unity 自动生成管理)
  5. Packages:包配置信息(Unity 自动生成管理)
  6. ProjectSettings:工程设置信息(Unity 自动生成管理)

Unity 界面基础

Scene 和 Hierarchy 窗口 知识点

Scene 和 Hierarchy

场景窗口和层级窗口是息息相关的
层级窗口中看到的内容就是场景窗口中的显示对象

Hierarchy 层级窗口

我们可以在 Hierarchy 窗口中创建或拖入各种游戏对象比如:模型、光源、图片 UI 等等内容

层级窗口中显示的,就是一个场景中的所有对象

在此窗口右键,可创建一些简单的 3D 物体,并对对象进行一些简单操作(复制,粘贴,克隆等)。

在物体前,可以选择是否显示和是否可选。

Scene 场景窗口

窗口上方工具条内容

  1. 渲染模式
    1. Shaded:着色器模式
    2. Wireframe:网格模式
  2. 2D、3D 视图切换
  3. 光源、音效、特效显示开启
  4. 辅助线显示
  5. Gizmos:辅助功能,控制场景上提示图标等
  6. 搜索栏
  7. 场景轴向

坐标轴

关于 Unity 中的 3D 世界坐标轴

红色为 X 轴正向,绿色为 Y 轴正向,蓝色为 Z 轴正向

以屏幕为参照物:垂直屏幕向内为 Z 正方向,平行屏幕向右为 X 正方向,平行屏墓向上为 Y 正方向

点击坐标轴中间方块,变为 ISO(正交试图)

点击不同坐标轴,切换到对应视图(正视图,侧视图,俯视图等)

快捷工具栏

Q:手动工具,可使用鼠标左键平移视角

W:移动工具,物体移动

E:旋转工具,物体旋转

R:缩放工具,物体缩放

T:矩形工具(2D)

Y:综合工具,随意选择、移动、旋转、缩放物体

Global/Local:世界/本地坐标系,旋转时,轴的相对目标

Global/Local 右边那个:移动物体时,一次只移动一步长(米),步长可在。

快捷键操作

左键相关

鼠标单击:选中单个物体

鼠标框选:选中多个物体

Ctrl+鼠标单击:多选物体

鼠标右键按下+移动鼠标:旋转视口

长按 ALT 键+鼠标左键+移动鼠标:相对观察视口中心点旋转

右键相关

鼠标右键按下+移动鼠标:旋转视口心

鼠标右键按下+WASD:漫游场景

鼠标右键按下+WASD+Shift:快速漫游场景

长按 ALT 键+鼠标右键+移动鼠标:相对屏幕中心点拉近拉远

中键相关

滚动鼠标中间:相对屏幕中心点拉近拉远

鼠标中间按下+移动鼠标:平移观察视口

长按 ALT 键+滚动鼠标中间:鼠标指哪就朝哪拉近拉远

其他

选中物体之后,按 F 键:居中显示物体
(或者在层级窗口中双击对象)

总结

所有的游戏对象都会出现在

Scene 场景窗口和 Hierarchy 层级窗口中需要掌握的就是熟练快捷的在

Scene 窗口中操作游戏物体(位置旋转缩放)

Game 和 Project 窗口 知识点

Game 游戏窗口

Game 游戏窗口中显示的内容;

是场景中摄像机,拍摄范围内的游戏对象是玩家通过屏幕能看到的东西

1.引擎中运行游戏

⒉ 暂停运行

3.逐帧运行

Display1:显示设备选择

FreeAspect:屏幕分辨率

Scale:缩放(一般为 1)

Maximize On Play:运行时全屏

Stats:渲染统计信息

Project 工程窗口

窗口中显示的内容主要是,Assets 文件夹中的所有内容。

主要用来管理资源、脚本文件,所有游戏资源都会显示在该窗口中。

在此窗口中,右键资源,可以对其进行一些操作。

常用的有

Show In Explorer:将当前选中的资源,在文件资源管理器下显示

Create:创建一个指定类型的资源

Project 工程窗口默认文件夹

默认文件夹(老版本没有)

Assets:资源文件夹,游戏资源全部都放这里。

Scenes:里面有一个默认空场景

Packages:官方拓展包

Project 工程窗口关键功能

  1. 窗口设置:右上角三点
  2. 创建相关资源文件:左上角+,或者右键 create
  3. 查找
  4. 按资源类型查找:t:type
  5. 按名字查找

资源类型

图片格式:jpg、png、tga

模型格式:fbx、max、maya

音效:wav、mp3、ogg

文本:txt、json、bytes

视频:mp4

总结

Game 游戏界面,是玩家玩游戏看到的画面

界面中重要功能是:分辨率设置项、渲染统计界面

Project 工程界面,是我们用于管理游戏资源和代码的

重要的是知道支持的主要资源类型

Inspector 和 Console 窗口 知识点

Inspector 和 Console

Inspector 检查窗口:查看场景中游戏对象关联的 C#脚本信息

Console 控制台窗口:用于查看调试信息的窗口;报错、警告、测试打印都可以显示在其中

Inspector 检查窗口

不选择场景中游戏对象或,不进行任何相关设置,该界面不会显示任何信息

当选择场景中意游戏对象时,该界面将显示和该游戏对象,关联的 C#脚本信息

红色部分:游戏对象基本设置

Cube-对象名字;Static-静态;Tag-标签;Layer-层级;

蓝色部分:关联的 C#脚本

Transform-位置旋转缩放信息组件;

绿色部分:脚本的公共成员变量

Add Component:添加组件

image-20230206212132572

Console 控制台窗口

默认未开启,可以在 Window——>General 中开启,或者使用快捷键:Ctrl+Shift+C

该窗口,将显示代码编译过程中,或者游戏运行过程中的报错、警告、测试信息,主要用于查错和调试用。

  1. 清空控制台
  2. 相同内容折叠显示
  3. 运行时清空
  4. 构建时清空
  5. 报错暂停运行
  6. 是否显示错误信息
  7. 是否显示警告信息
  8. 是否显示打印信息
image-20230206213323118

总结

  1. Inspector 检查窗口:用于设置游戏对象具体信息
  2. Console 控制台窗口:用于显示调试信息,报错、警告、打印信息等
  3. Scene 场景窗口:所有游戏对象所在地
  4. Hierarchy 层级窗口:所有游戏的对象名单
  5. Game 游戏窗口:玩家看到的游戏画面
  6. Project 工程窗口:所有游戏资源和脚本内容

在 Unity 中做游戏就像在拍戏

Scene 是舞台,所有演员都在舞台上

Hierarchy 是舞台演员名单

Game 是摄像机拍到的画面

Inspector 可以看到每个演员的剧本,它要扮演什么角色

Project 是后台,所有未上场的演员和没有使用的剧本都在这里

Console 是表演过程中的信息反馈

练习题

在场景中用 Cube 堆砌一个,有 4 层 Cube 高的金字塔出来,摄像机要斜向下 45 度拍摄它。

工具栏和父子关系 知识点

工具栏

image-20230206220330717

  1. 文件操作:新建工程,新建场景,工程打包等等

    1. Build Settings:工程打包
  2. 编辑操作:对象编辑操作相关,工程设置,引擎设置相关

    1. shift+F:视角锁定对象
    2. ctrl+alt+数字:将选中的物体编组
    3. ctrl+shift+数字:选中编组中的物体
    4. Project Settings:项目设置,对应工程文件夹的 ProjectSettings 文件夹
    5. Preferences:首选项,包含很多编辑器的设置
    6. Shortcuts:快捷键设置
    7. Grid and Snap Settings:步长设置
  3. 资源操作:基本等同于Project 窗口中右键相关功能,其他功能为导入相关的功能。

  4. 对象操作:基本等同于 Hierarchy 窗口中右键相关功能

    1. MoveToVie:将选中物体移动到视线中心
    2. AlignWithView:将选中物体移动到视角位置
    3. AlignVieToSelected:将视角移动到选中的物体位置
    4. ToggleActiveState:激活/失活对象
  5. 脚本操作:Unity 自带的脚本,可以添加各系统中的脚本

  6. 窗口:可以打开 Unity 各核心系统的窗口

    注意:AssetStore 在高版本 Unity 已经没有了

  7. 帮助:检查更新,查看版本等等功能

    1. UnityManual:Unity 使用手册
    2. ScriptingReference:脚本使用说明

File 中的重要选项:BuildSetting(工程发布打包)

Edit 中的重要选项:Project Setting(工程各系统设置);Preferences(首选项,可以设置编程软件)

GameObject 中的重要选项:MoveToView、Align With View、Align View to Selected(几种快捷设置位置的功能)

MoveToView:ctrl+alt+F

Align With View:ctrl+shift+F

对象的父子关系

对象在另一个对象的右下,就说明他们存在父子关系

  1. 子对象会随着父对象的变化而变化
  2. 子对象 Inspector 窗口中 Transform 信息是相对父对象的
    1. 选择 Inspector 的 debug 模式,可以看到子对象是 Local 坐标,即相对于父对象的坐标
  3. Pivot:锚点,默认为本身,但会根据子对象改变
  4. Global:全局,世界坐标,一般来说,对象在世界坐标,对象根据世界坐标操作。
  5. Local:本地坐标,对象根据自身的本地坐标操作

总结

  1. 上方工具栏:

    目前只需要记住工具栏中的几个重要选项即可

    哪里打包程序?哪里设置编程软件?哪里可以打开其它窗口?

  2. 父子关系:

    对象之间产生父子关系后,如何相互影响?

    爸爸干嘛,儿子干嘛

    儿子干嘛,爸爸不管

习题

在场景中用自带几何体拼一个坦克出来,坦克有身体、炮台、炮管、四个轮子。
他们有共同的父对象,父对象移动,其它部件跟随移动,炮管跟着炮台转向。

Unity 工作原理

反射机制和游戏场景 知识点

1.Unity 中的反射机制

Unity 引擎本质是一个软件,使用它时是处于运行中的
我们是在一个运行中的软件里制作游戏的,Unity 开发的本质就是
在 Unity 引擎的基础上,利用反射和引擎提供的各种功能,进行的拓展开发

回忆一下反射的概念

程序正在运行时,可以查看其它程序集或者自身的元数据

一个运行的程序查看本身或者其它程序的元数据的行为就叫做反射

在程序运行时,通过反射可以得到其它程序集或者自己程序集中
代码的各种信息,比如类,函数,变量,对象等等
我们可以实例化它们,执行它们,操作它们

场景中对象的本质是什么

GameObject 类对象是 Unity 引擎提供给我们的
作为场景中所有对象的根本
在游戏场景中出现一个对象
不管是图片、模型、音效、摄像机等等都是
依附于GameObject 对象

拟人化记忆:GameObject 就是没有剧本的演员

Transform 是什么

GameObject 对象作为一个出现在舞台(3D 场景)中的演员
必须有一个表示自己所在位置的信息
Transform 就是一个必不可少的剧本
它的本质就是发了一本表示位置的剧本给演员
(相当于就是用一个 Transform 类对象和 GameObject 类对象进行关联)
用于设置和得到演员在世界中的位置角度缩放等信息

反射机制的体现

除了 Transform 这个表示位置的标配剧本外
我们可以为这个演员(GameObject)关联各种剧本(C#脚本)
让它按照我们剧本中(代码逻辑中)的命令来处理事情
而为演员添加剧本的这个过程,就是在利用反射 new 一个新的剧本对象和演员(GameObject)对象进行关联,让其按我们的命令做事。

图示表现

image-20230207172851069

举例体现

前提:Unity 帮助我们实现了对象查找和关联

  1. 修改 Inspector 面板中 Transform 的内容

    利用反射已知对象,类名,变量名,通过反射为该对象设置变量

  2. 新建一个脚本后,添加给一个指定的 GameObject 对象

    利用反射已知类名,可以获取所有公共成员,故可以在 Inspector 面板上创建各公共字段信息

2.游戏场景

基本知识点

  1. 游戏场景的保存:ctrl+s

  2. 游戏场景的新建:在 Project 窗口 Assets->Scenew 文件夹下创建

  3. 多个游戏场景叠加显示:

    可将多个场景拖到 Hierarchy 窗口,进行同时渲染(没啥用,最多可以复制两个场景不同的东西)

游戏场景的本质

游戏场景文件,后缀为.unity

它的本质就是一个配置文件

Unity 有一套自己识别处理它的机制

但是本质就是把场景对象相关信息读取出来

通过反射来创建各个对象关联各个脚本对象

简单来说:

就是 Unity 通过读取写入这个场景文件,获取对象信息,在 Unity 运行时,用 C#反射来实例化并编辑对象

这些对象的所有字段(成员变量)、组件,都在场景文件当中

比如下面的 MyCube,它本身是一个 GameObejct,有一个 TransForm 组件

这个 GameOjbect 的 m_Name 是 MyCube

其他字段参考:UnityEngine.GameObject - Unity 脚本 API

也就是说,我们可以直接修改场景文件,修改后 Unity 会提示你重新加载场景,你的修改就会被体现

image-20230207201038126

image-20230207200403732

Unity 脚本基础

预设体和资源包的导入导出 知识点

1.预制体

什么是预制体?

Prefab:预先制作好的的物体,文件后缀.prefab

Prefab 在 Scene 当中的所有实例都是一样的

我们可以将在场景当中配置好的物体,拖入到 Assets->Prefabs 文件夹当中,使得其变为一个预制体(prefab)

image-20230207204550367

预制体的本质

我们可以文本形式打开.prefab 文件,发现它和场景文件(.unity)文件一样,是一个配置文件。

原理同场景文件。

注意:中文在这些配置文件中以 Unicode 编码体现,所以不能直接查找到中文

image-20230207204916608

Prefab 的修改

在 Scene 窗口修改(添加东西)Prefab 之后,在 Inspector 窗口

选择Overrides

Revert All(重置所有):重置所有预制体实例为初始(本来)状态。

Apple All(应用所有):应用修改到所有预制体实例

Open Prefab:在 Project双击或者在 Inspector Open预制体,打开预制体修改模式,这是用来专门修改预制体的,所有修改都会应用。

UnPack Prefab

选中物体右键后,可以选择 UnPack Prefab,解除与预制体的关系。

实例物体右键->Prefab

有一些有关于物体的 Prefab 的操作,比如 Select Assets(选中预制体源文件)

2.导入导出资源包

在 Project 窗口,右键后可以选择导入(Import Package)导出(Export Package)资源包。

资源包文件后缀:**.unitypackage**

脚本基本规则 知识点

1.创建规则

  1. 不在 VS 中创建脚本了

  2. 可以放在 Assets 文件夹下的任何位置(建议同—文件夹管理,一般使用一个 Scripts 文件夹管理)

  3. 类名和文件名必须—致,不然不能挂载(因为反射机制创建对象,会通过文件名去找 Type)

    挂载:将此脚本作为一个组件添加到某个物体当中

  4. 建议不要使用中文名命名

  5. 没有特殊需求不用管命名空间

  6. 创建的脚本默认继承 MonoBehavior

2.MonoBehavior 基类

  1. 创建的脚本默认都继承 MonoBehaviour,继承了它才能够挂载在 GameObject 上

  2. 继承了 MonoBehavior 的脚本不能 new 只能挂!!! ! !! !!

    Unity 的坑——避免用 New 来创建继承于 MonoBehaviour 脚本的对象_弹吉他的小刘鸭的博客-CSDN 博客_unity 銝要 ew

  3. 继承了 MonnBehavior 的脚本不要去写构造函数,因为我们不会去 new 它,写构造函数没有任何意义

  4. 继承了 MonoBehavior 的脚本可以在一个对象上挂多个(如果没有加 DisallowMultipleComponent 特性)

    DisallowMultipleComponent 特性:不允许挂载多个相同脚本。

  5. 继承 MonoBehavior 的类也可以再次被继承,遵循面向对象继承多态的规则

    再次继承后的类脚本,也可以挂载到物体上。

3.不继承 MonoBehavior 的类

  1. 不继承 Mono 的类不能挂载在 GameObject 上
  2. 不继承 Mono 的类想怎么写怎么写,如果要使用需要自己 new
  3. 不继承 Mono 的类一般是单例模式的类(用于管理模块)或者数据结构类(用于存储数据)
  4. 不继承 Mono 的类不用保留默认出现的几个函数

4.脚本执行的顺序

Project Settings->Script Execution Order

或者选择脚本->Inspector->Execution Order(右上角)

可以设置脚本执行顺序

5.默认脚本内容

脚本模板路径

1
EditorVersion\Editor\Data\Resources\ScriptTemplates

修改对应模板文件即可。

习题

  1. 为什么继承 MonoBehavior 的类不能写构造函数?

    因为继承 MonoBehavior 的类不能 new,构造函数也就没有意义。

    Unity 会在这种类的构造函数进行一些内部的操作,如果强行 new 出来,会被 UnityObject 重写 ToString,得到一个”null”字符串,但是仍然可以调用一些函数。

  2. 为什么继承 MonoBehavior 的类不能直接 new 出来用,要挂载?

    由于继承 MonoBehavior 的类,都是组件,而组件就是设计用来挂载的。

    The MonoBehaviour base class is the only base class from where you can derive your own components from

    Unity 的代码看似用的 C#,实际上底层是翻译成 C++使用的,所以不能完全从.Net 的角度来看 Unity 的设计思路,Unity 为什么这么设计语言,我也没有找到定论,这里只能做一个大概的猜测,猜测这么做的原因主要有两点:

    1、在 C#里,对于 Object,用 new 关键字来创建对象,会调用该类的构造函数,但 Unity 引擎对于 MonoBehaviour 类的对象,需要利用其构造函数做一些引擎内的事情,所以不提倡使用 new 关键字调用其构造函数,而是用 Awake 和 Start 函数来代替构造函数的功能。举个例子,下面部分是 MonoBehaviour 在 C#这边的部分源码,可以看到,引擎自己在里面使用了构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // MonoBehaviour.bindings.cs文件夹
    namespace UnityEngine
    {
    // MonoBehaviour is the base class every script derives from.
    [RequiredByNativeCode]
    [ExtensionOfNativeClass]
    [NativeHeader("Runtime/Mono/MonoBehaviour.h")]
    [NativeHeader("Runtime/Scripting/DelayedCallUtility.h")]
    public class MonoBehaviour : Behaviour
    {
    public MonoBehaviour()
    {
    ConstructorCheck(this);
    }
    ...
    }
    }

    2、把 ScriptComponents 组件统一用 AddComponent 或 Instantiate 函数来创建,可能是一种类似工厂模式的设计理念,旨在统一管理对象的创建。

    Unity 的坑——避免用 New 来创建继承于 MonoBehaviour 脚本的对象unity 銝要 ew弹吉他的小刘鸭的博客-CSDN 博客

  3. 请写一个类用于说明不继承 MonoBehavior 的类对于我们的作用

    管理资源的类,一般不需要继承 MonoBehavior,因为它不需要挂载到场景当中,作为组件存在。

    数据结构类一般也不需要继承 MonoBehavior,因为数据只需要读取,也不需要挂载到场景当中,作为组件存在。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class SceneMgr()
    {
    private static SceneMgr instance = new SceneMgr();
    private SceneMgr() {}
    public static SceneMgr Instance
    {
    get => this;
    }

    public void Func() { ... }
    }

唐老狮答案

第 1 题

因为 MonoBehaviour 不能 new 只能挂我们不会通过构造函数实例化对象写构造函数没有意义

第二题

  1. 这是 Unity 定下的规则;
  2. 继承 MonoBehavior 的类只能挂载在 GameObject 上配合使用

第三题

数据结构类或者单例管理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//玩家信息类
public class PlayerInfo
{
public string name;
public bool sex;

public int atk;
public int def;
public int hp;
public int maxHp;
}
//怪物管理类
public class MonsterMgr
{
private static MonsterMgr instance = new MonsterMgr();
public static MonsterMgr Instance => instance;
}

生命周期函数 知识点

知识点一 了解帧的概念

什么是帧?

游戏的本质就是一个死循环;每—次循环处理游戏逻辑,就会更新一次画面。

之所以能看到画面在动,是因为,切换画面的速度到达—定时人眼就认为画面是流畅的。

—帧就是执行一次循环。

FPS(Frame Per Second):即每秒帧数。

人眼舒适放松时可视帧数是每秒 24 帧

电影就是 24 帧的

游戏卡顿的原因:跑 1 帧游戏逻辑中的计算量过大,或者 CPU 不给力,不能在—帧的时间内处理完所有游戏逻辑。

Unity 当中的游戏死循环

Unity 底层已经帮助我们做好了死循环,我们需要学习 Unity 的生命周期函数,利用它做好的规则来执行我们的游戏逻辑就行了。

知识点二 生命周期函数的概念

所有继承 MonoBehavior 的脚本 最终都会挂载到 GameObject 游戏对象上。

生命周期函数 就是该脚本对象依附的 GameObject 对象从出生到消亡整个生命周期中,会通过反射自动调用的一些特殊函数。

Unity 帮助我们记录了一个 GameObject 对象依附了哪些脚本;

会自动的得到这些对象,通过反射去执行一些固定名字的函数

知识点三 生命周期函数

物理碰撞触发相关函数将在后讲解

注意:

生命周期函数的访问修饰符一般为privateprotected

因为不需要在外部自己调用生命周期函数,都是 Unity 自己帮助我们调用的。

这是一种观察者模式。

Awake

出生时调用,类似构造函数一个对象只会调用一次

当对象(自己这个类对象)被创建时 才会调用该生命周期函数。

类似构造函数的存在,我们可以在一个类对象刚创建进行一些初始化操作

Awake 在加载脚本实例时调用。

MonoBehaviour-Awake() - Unity 脚本 API

1
2
3
private virtual void Awake() {

}

在 Unity 中打印信息的两种方式

  1. 没有继承 MOnoBehavior 类的时候,使用

    1
    Debug.Log(str);
  2. 继承了 MonoBehavior,有一个现成的方法可以使用

    1
    print(str);

OnEnable

依附的 GameObject 对象每次激活时调用

对于我们来说,想要当一个对象被激活时,进行一些逻辑处理,就可以写在这个函数。

该函数在对象变为启用和激活状态时调用。

MonoBehaviour-OnEnable() - Unity 脚本 API

Start

从自己被创建出来后,第一次帧更新之前调用,—个对象只会调用一次

主要作用还是用于初始化信息的,但是它相对 Awake 来说 要晚一点。

因为它是在对象,进行第一次帧更新之前才会执行的。

在首次调用任何 Update 方法之前启用脚本时,在帧上调用 Start。

MonoBehaviour-OnEnable() - Unity 脚本 API

FixedUpdate

物理帧更新,固定间隔时间执行,间隔时间可以设置。

它主要是用于进行物理更新,它是每一帧的执行的。但是,这里的帧和游戏帧有点不同。

它的时间间隔是可以在 ProjectSettings 中的 Time 里去设置的。

Update

逻辑帧更新每帧执行。

主要用于处理游戏核心逻辑更新的函数。

如果启用了 MonoBehaviour,则每帧调用 Update。

在实现任何类型的游戏脚本时,Update 都是最常用函数。 但并非所有 MonoBehaviour 脚本都需要 Update。

MonoBehaviour-Update() - Unity 脚本 API

LateUpdate

每帧执行,于 Update 之后执行。

一般这个更新是用来处理 摄像机位置更新相关内容的;

Update 和 LateUpdate 之间 Unity 进了一些处理,处理我们动画相关的更新。

摄像机的控制一般写在这里,因为要等渲染完成之后才查看。

如果启用了 Behaviour,则每帧调用 LateUpdate。

LateUpdate 在调用所有 Update 函数后调用。 这对于安排脚本的执行顺序很有用。例如,跟随摄像机应始终在 LateUpdate 中实现, 因为它跟踪的对象可能已在 Update 中发生移动。

MonoBehaviour-LateUpdate() - Unity 脚本 API

OnDisable

依附的 GameObject 对象每次激活时调用。

如果我们希望在一个对象失活时做一些处理 就可以在该函数中写逻辑。

该函数在行为被禁用时调用。

当对象销毁时也会调用该函数,它可用于任何清理代码。 当编译完成后重新加载脚本时,将调用 OnDisable,并在加载脚本后调用 OnEnable。

MonoBehaviour-OnDisable() - Unity 脚本 API

OnDestroy

对象销毁时调用,依附的 GameObject 对象被删除时。

注意:调用此方法,将会调用一次 OnDisable,因为此时对象失活一次。

销毁附加的行为将导致游戏或场景收到 OnDestroy

MonoBehaviour-OnDestroy() - Unity 脚本 API

知识点四 生命周期函数 支持继承多态

即使父类脚本没有挂载,子类挂载了,也可以执行父类方法。

总结

这些生命周期函数,如果你不打算在其中写逻辑,那就不要在这些出生命周期函数。

因为如果你重写了这些函数,即使里面没有内容,Unity 也会通过反射获取并执行,增加一些额外性能消耗。

其他:

  1. 脚本可以挂载到场景当中的任意对象当中。
  2. 一个对象挂载的脚本,就实例化了一个类,是一份独特的唯一实例,不和这个类的其他实例互通(静态除外)。
  3. 当场景当中的脚本一开始就处于失活状态时,不会调用生命周期函数。

练习题

  1. 生命周期函数都在何时执行,执行的先后顺序是什么?

    Awake:在加载脚本实例时调用(类似构造函数)

    OnEnable:在脚本实例被激活是调用。

    Start:在游戏帧更新开始前调用。

    FixedUpdate:物理帧更新,根据设定的物理时间调用。

    Update:游戏帧更新,游戏中每一帧调用。

    LateUpdate:在所有 Update 执行完成后调用,可以用来设置摄像机跟随。

    OnDisable:在脚本实例失活时调用。

    OnDestroy:在对象被销毁时调用。

  2. 生命周期函数并不是基类中的成员,为什么 Unity 可以自动的执行这些特殊的函数。

    通过反射。

答案

2:

Unity 帮助我们记录了场景上的所有 GameObject 对象以及各个关联的脚本对象;

在游戏执行的特定时机(对象创建时,失活激活时,帧更新时);

它会通过函数名反射得到脚本对象中对应的生命周期函数,然后在这些特定时机执行他们。

脚本基本规则补充 关于继承 Mono 的类的构造函数

我们要知道,虽然建议大家不在继承 MonoBehavior 的类中写构造函数

但是不意味着我们不能写,当我们在继承 MonoBehavior 的类中写无参构造函数时,你会发现在编辑模式下或者运行后,只要该脚本挂载在场景中,那么该无参构造函数是会被自动执行的。

因为 Unity 的工作原理中提到的反射机制,Unity 实际上通过反射帮助我们实例化了该脚本对象,既然要实例化那么肯定是需要 new 的,只不过 Unity 中不需要我们自己 new 继承了 MonoBehavior 的类,只要挂载后 Unity 帮助我们做了这件事。

那么为什么不建议大家写构造函数呢?

1.Unity 的规则就是,继承 MonoBehavior 的脚本不能 new 只能挂载

2.生命周期函数的 Awake 是类似构造函数的存在,当对象出生就会自动调用

3.写构造函数反而在结构上会破坏 Unity 设计上的规范

总结:

如果继承 MonoBehavior 的脚本想要进行初始化相关,可以在 Awake 或者 Start 中进行,搞清这两个生命周期函数的执行时机,根据需求选择在哪里进行初始化。

切记!!继承 MonoBehavior 的脚本不要 new,不要 new,不要 new!!

不同对象的生命周期函数是在同一个线程中执行的吗?

答案:Unity 中所有对象上挂载的生命周期函数都是在一个主线程中按先后执行的

理解:Unity 会主动把场景上的对象,对象上挂载的脚本都统统记录下来,在主线程的死循环中,按顺序按时机的通过反射,执行记录的对象身上挂载的脚本的对应生命周期函数

Inspector 窗口可编辑的变量 知识点

Inspector 显示的可编辑内容就是脚本的成员变量

知识点一 私有和保护无法显示编辑

私有和保护无法显示在 Inspector 窗口当中进行编辑。

知识点二 让私有的和保护的也可以被显示

加上强制序列化字段特性

1
[SerializeField]

所谓序列化就是把一个对象保存到一个文件或数据库字段中去。

1
2
3
4
[SerializeField]
private int privateInt;
[SerializeField]
protected string protectedStr;

知识点三 公共的可以显示编辑

公共的字段可以直接在 Inspector 窗口编辑。

知识点四 公共的也不让其显示编辑

在变量前加上特性

1
2
[HideInInspector]
public int publicInt2 = 50;

知识点五 大部分类型都能显示编辑

1
2
3
4
5
6
7
8
9
10
public int[] array;
public List<int> list;
public E_TestEnum type;
public GameObject gameObj;

//字典不能被Inspector窗口显示
public Dictionary<int, string> dic;
//自定义类型变量
public MyStruct myStruct;
public MyClass myClass;

知识点六 让自定义类型可以被访问

加上序列化特性

1
[System.Serializable]

字典怎样都不行

知识点七 一些辅助特性

1.分组说明特性 Header

为成员分组,Header 特性

1
2
3
4
5
6
[Header("基础属性")]
public int age;
public bool sex;
[Header("战斗属性")]
public int atk;
public int def;

2.悬停注释 Tooltip

为变量添加说明

1
2
[Tooltip("闪避")]
public int miss;

3.间隔特性 Space()

让两个字段间出现间隔

1
2
[Space()]
public int crit;

4.修饰数值的滑条范围 Range

[Range(最小值, 最大值)]

1
2
[Range(0,10)]
public float luck;

5.多行显示字符串

默认不写参数显示 3 行,写参数就是对应行

1
2
[Multiline(5)]
public string tips;

6.滚动条显示字符串

默认不写参数就是超过 3 行显示滚动条

[TextArea(3, 4)]

最少显示 3 行,最多 4 行,超过 4 行就显示滚动条

1
2
[TextArea(3,4)]
public string myLife;

7.为变量添加快捷方法

ContextMenuItem

参数 1 显示按钮名

参数 2 方法名 不能有参数

[ContextMenuItem(“显示按钮名”, “方法名”)]

1
2
3
4
5
6
[ContextMenuItem("重置钱", "Test")]
public int money;
private void Test()
{
money = 99;
}

8.为方法添加特性能够在 Inspector 中执行

[ContextMenu(“测试函数”)]

1
2
3
4
5
[ContextMenu("哈哈哈哈")]
private void TestFun()
{
print("测试方法");
}

注意

  1. Inspector 窗口中的变量关联的就是对象的成员变量,运行时改变他们就是在改变成员变量
  2. 拖曳到 GameObject 对象后 再改变脚本中变量默认值 界面上不会改变
  3. 运行中修改的信息不会保存

练习题

  1. 如何让公共成员不在 lnspector 面板上设置
    如何让私有或保护成员可以在 lnspector 面板上设置

    1
    2
    3
    4
    [HideOnInspector]
    public int publicInt;
    [SerializeField]
    private int privateInt;
  2. 为什么加不同的特性,在 lnspector 窗口上会有不同的效果请说出你的理解

    因为 Unity 在通过使用反射加载类时,会判断拥有的特性,然后按对应特性实现对应功能。

答案

2.因为 Unity 中是通过反射得到类的信息,然后在 Inspector 窗口中显示字段信息。Unity 内部通过反射获取字段的特性,当具有一些特殊特性时,便会做对应的处理。

MonoBehavior 中的重要内容 知识点

继承链:

MonoBehavior->Behavior->Component->Object

所以,这期其实是讲的这些类当中的一些方法属性。

UnityEngine.MonoBehaviour - Unity 脚本 API

UnityEngine.Behaviour - Unity 脚本 API

UnityEngine.Component - Unity 脚本 API

UnityEngine.Object - Unity 脚本 API

知识点一 重要成员

1.获取依附的 GameObject

1
print(this.gameObject.name);

2.获取依附的 GameObject 的位置信息

1
2
3
4
//得到对象位置信息
print(this.transform.position); //位置
print(this.transform.eularAngeles); //角度
print(this.transform.lossyScale); //缩放大小

这种写法和上面是一样的效果,都是得到依附的对象的位置信息

1
this.gameObject.transform

3.获取脚本是否激活

1
this.enabled = false;

获取别的脚本对象 依附的 gameobject 和 transform 位置信息

1
2
print(otherLesson3.gameObject.name);
print(otherLesson3.transform.position);

知识点二 重要方法

1.得到自己挂载的单个脚本

根据脚本名获取

获取脚本的方法,如果获取失败,就是没有对应的脚本,会默认返回空。

1
Lesson3_Test t = this.GetComponent("Lesson3_Test") as Lesson3_Test;

根据 Type 获取

1
t = t.GetComponent(typeof(Lesson3_Test)) as Lesson3_Test;

根据泛型获取,建议使用泛型获取,因为不用二次转换

1
t = this.GetComponent<Lesson3_Test>();

只要你能得到场景中别的对象或者对象依附的脚本

那你就可以获取到它的所有信息

2.得到自己挂载的多个脚本

1
2
3
4
Lesson3[] array = this.GetComponents<Lesson3>();

List<Lesson3> list = new List<Lesson3>();
this.GetComponents<Lesson3>(list);

3.得到子对象挂载的脚本(它默认也会找自己身上是否挂载该脚本)

函数是有一个参数的,默认不传 是 false,意思就是,如果子对象失活,是不会去找这个对象上是否有某个脚本的。如果传 true 即使失活 也会找。

得子对象挂载脚本,单个

1
t = this.GetComponentInChildren<Lesson3_Test>(true);

得子对象挂载脚本,多个

1
2
3
4
Lesson3_Test[] lts = this.GetComponentsInChildren<Lesson3_Test>(true);

List<Lesson3_Test> list2 = new List<Lesson3_Test>();
this.GetComponetsInChildren<Lesson3_Test>(true, list2);

4.得到父对象挂载的脚本(它默认也会找自己身上是否挂载该脚本)

1
2
3
4
t = this.GetComponentInParent<Lesson3_Test>();

lts = this.GetComponentsInParent<Lesson3_Test>();
this.GetComponentsInParent<Lesson3_Test>(lts);

5.尝试获取脚本

提供了一个更加安全的,获取单个脚本的方法。如果得到了,会返回 true。

然后再来进行逻辑处理即可。

1
2
3
if (this.TryGetComponent<Lesson3_Test>(out l3t)) {
...
}

练习题

第一题

请说出一个继承了 MonoBehavior 的脚本中
this、this.gameObject、this.transform 分别代表什么?

this:代表自己;this.gameObject:依附的对象;this.transform:依附的对象上的 transform。

第二题

一个脚本 A,一个脚本 B,他们都挂在一个 GameObject 上

实现在 A 中的 Start 函数中让 B 脚本失活,请用代码失活。

1
2
3
4
5
6
7
8
9
10
11
public class A : MonoBehaviour
{
private void Start()
{
B bComp = this.GetComponent<B>();
if (bComp != null)
{
bComp.enabled = false;
}
}
}

第三题

—个脚本 A 一个脚本 B,脚本 A 挂在 A 对象上,脚本 B 挂在 B 对象上。

实现在 A 脚本的 Start 函数中将 B 对象上的 B 脚本失活。

1
2
3
4
5
6
7
8
9
10
11
public class A : MonoBehaviour
{
public GameObject bObj;
private void Start()
{
if (bObj != null)
{
bObj.GetComponent<B>().enabled = false;
}
}
}

基础知识 总结

学习 Unity,其实首先是学习 Unity 编辑器的基本使用。

然后学习 Unity 自带的脚本,如果 GameObject,Transform 等等。

当 Unity 自带脚本不能满足我们自己的需求时,就需要自定义脚本。

自定义脚本往往实现我们自己想要的效果。

当游戏运行后,挂载到物体上的脚本其实会通过反射实例化为一个个对象。

每一个物体上的脚本都是独立的一个个对象。

Unity 重要组件和 API

GameObject 成员变量 知识点

继承链:

GameObject->Object

所以,这期其实是讲的这些类当中的一些方法属性。

UnityEngine.Object - Unity 脚本 API

UnityEngine.GameObject - Unity 脚本 API

知识点一 重要成员(字段)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//名字
print(this.gameObject.name);
this.gameObject.name = "Panzi";
print(this.gameObject.name);
//是否激活
print(this.gameObject.activeSelf);
//是否是静态
print(this.gameObject.isStatic);
//层级
print(this.gameObject.layer);
//标签
print(this.gameObject.tag);
//transform
//this.transform 上一节课讲解的 通过Mono去得到的依附对象的GameObject的位置信息
//他们得到的信息是一样 都是依附的GameObject的位置信息
print(this.gameObject.transform.position);

知识点二 GameObject 中的静态方法

创建自带几何体

1
2
3
4
//只要得到了一个GameObject对象 我就可以得到它身上挂在的任何脚本信息
//通过obj.GetComponent来得去
GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
obj.name = "唐老狮创建的立方体";

查找对象相关的知识

  1. 无法找到失活的对象,只能找到激活的对象。
  2. 如果场景中存在多个满足条件的对象,我们无法准确确定找到的是谁。

得到某一个单个对象 目前有 2 种方式了

  1. 是 public 从外部面板拖 进行关联
  2. 通过 API 去找

通过对象名查找

通过对象名无法查找到多个对象,如果场景中存在多个满足条件的对象,我们无法准确确定找到的是谁。

1
2
3
4
5
6
7
8
9
10
11
12
//通过对象名查找
//这个查找效率比较低下 因为他会在场景中的所有对象去查找
//没有找到 就会返回null
GameObject obj2 = GameObject.Find("唐老狮");
if( obj2 != null )
{
print(obj2.name);
}
else
{
print("没有找到对应对象");
}

通过 tag 来查找对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//GameObject obj3 = GameObject.FindWithTag("Player");
//该方法和上面这个方法 效果一样 只是名字不一样而已
GameObject obj3 = GameObject.FindGameObjectWithTag("Player");
if (obj3 != null)
{
print("根据tag找的对象" + obj3.name);
}
else
{
print("根据tag没有找到对应对象");
}

//通过tag找到多个对象
//它也是 只能找到 激活对象 无法找到失活对象
GameObject[] objs = GameObject.FindGameObjectsWithTag("Player");
print("找到tag为Player对象的个数" + objs.Length);

其他方法

还有几个查找对象相关是用的比较少的方法 是 GameObject 父类 Object 提供的方法

1
2
3
4
5
//它可以找到场景中挂载的某一个脚本对象
//效率更低 上面的GameObject.Find 和通过FindWithTag找 只是遍历对象
//这个方法 不仅要遍历对象 还要遍历对象上挂载的脚本
Lesson4 o = GameObject.FindObjectOfType<Lesson4>(); //不止这一个
print(o.gameObject.name);

额外知识补充

Unity 中的 Object 和 C#中的万物之父的区别:

Unity 里面的 Object 不是指的万物之父 object;

Unity 里的 Object 命名空间在 UnityEngine 中的 Object 类,也是集成万物之父的一个自定义类。

C#中的 Object 命名空间是在 System 中的

就是两个不同命名空间的同名类,与 Unity 相关的类都继承 UnityEngine 命名空间中的基类

实例化对象(克隆对象)的方法

实例化(克隆)对象 它的作用 是根据一个 GameObject 对象 创建出一个和它一模一样的对象

1
2
3
4
5
GameObject obj5 = GameObject.Instantiate(myObj);
//以后学了更多知识点 就可以在这操作obj5
//如果你继承了 MonoBehavior 其实可以不用写GameObject一样可以使用
//因为 这个方法时Unity里面的 Object基类提供给我们的 所以可以直接用
//Instantiate(myObj);

删除对象的方法

删除对象有两种作用:

  1. 是删除指定的一个游戏对象
  2. 是删除一个指定的脚本对象

注意:这个 Destroy 方法 不会马上移除对象,只是给这个对象加了一个移除标识;
一般情况下 它会在下一帧时把这个对象移除并从内存中移除

如果没有特殊需求 就是一定要马上移除一个对象的话,建议使用上面的 Destroy 方法

因为 是异步的 降低卡顿的几率。

1
2
3
4
5
6
7
8
9
10
//是UnityEngine.Object当中的方法
GameObject.Destroy(myObj2);
//第二个参数 代表延迟几秒钟删除
GameObject.Destroy(obj5, 5);
//Destroy不仅可以删除对象 还可以删除脚本
//GameObject.Destroy(this);

//如果是继承MonoBehavior的类 不用写GameObject
//Destroy(myObj2);
//DestroyImmediate(myObj);

下面这个方法 就是立即把对象 从内存中移除 C 了

1
GameObject.DestroyImmediate(myObj);	//立即删除这个对象

过场景不移除

默认情况,在切换场景时,场景中对象都会被自动删除掉;

如果你希望某个对象,过场景不被移除。下面这句代码 就是不想谁过场景被移除,就传谁。

一般都是传依附的GameObject 对象

比如下面这句代码的意思 就是自己依附的 GameObject 对象 过场景不被删除

1
2
3
4
//是UnityEngine.Object当中的方法
GameObject.DontDestroyOnLoad(this.gameObject);
//如果继承MOnoBehavior也可以直接写
//DontDestroyOnLoad(this.gameObject);

知识点三 GameObject 中的成员方法

创建空物体

new 一个 GameObject 就是在创建一个空物体

1
2
3
GameObject obj6 = new GameObject();
GameObject obj7 = new GameObject("唐老狮创建的空物体");
GameObject obj8 = new GameObject("顺便加脚本的空物体", typeof(Lesson2),typeof(Lesson1));

为对象添加脚本

继承 MOnoBehavior 的脚本 是不能够去 new ;

如果想要动态的添加继承 MonoBehavior 的脚本,在某一个对象上,直接使用 GameObject 提供的方法即可

1
2
3
4
5
6
Lesson1 les1 = obj6.AddComponent(typeof(Lesson1)) as Lesson1;
//用泛型更方便
Lesson2 les2 = obj6.AddComponent<Lesson2>();
//通过返回值,可以得到加入的脚本信息
//来进行一些处理
//得到脚本的成员方 和继承Mono的类得到脚本的方法 一模一样

标签比较

下面两种比较的方法是一样的.

1
2
3
4
5
6
7
8
if(this.gameObject.CompareTag("Player"))
{
print("对象的标签 是 Player");
}
if(this.gameObject.tag == "Player")
{
print("对象的标签 是 Player");
}

设置激活失活

1
2
3
4
5
//false 失活
//true 激活
obj6.SetActive(false);
obj7.SetActive(false);
obj8.SetActive(false);

广播消息

次要的成员方法 了解即可,不建议使用。

强调:下面讲的方法,都不建议大家使用,效率比较低。

通过广播或者发送消息的形式,让自己或者别人 执行某些行为方法。

通知自己执行什么行为。

命令自己,去执行这个 TestFun 这个函数,会在自己身上挂在的所有脚本去找这个名字的函数。

它会去找到,自己身上所有的脚本,有这个名字的函数去执行。

1
2
3
4
5
6
7
8
this.gameObject.SendMessage("TestFun");
this.gameObject.SendMessage("TestFun2", 199);

//广播行为 让自己和自己的子对象执行
//this.gameObject.BroadcastMessage("函数名");

//向父对象和自己发送消息 并执行
//this.gameObject.SendMessageUpwards("函数名");

总结

GameObject 的常用内容

基本成员变量:名字,失活激活状态,标签,层级等等。

静态方法相关:创建自带几何体,查找场景中对象,实例化对象,删除对象,过场景不移除。

成员方法:为对象动态添加指定脚本,设置失活激活的状态,和 MonoBehavior 中相同的得到脚本相关的方法。

练习题

第一题

1.一个空物体上挂了一个脚本,游戏运行时该脚本可以实例化出之前的坦克预设体

1
2
3
4
5
6
7
8
9
public class Lesson4_P1 : MonoBehaviour
{
public GameObject tankPrefab;
// Start is called before the first frame update
void Start()
{
Instantiate(tankPrefab);
}
}

答案

1
2
3
4
5
6
7
8
9
10
private void Start()
{
GameObject tank = GameObject.Find("tank");
if (tank != null)
{
//找到场景当中的坦克再创建
Instantiate(tank);
}

}

第二题

2.—个脚本 A,─ 个脚本 B,脚本 A 挂在 A 对象上,脚本 B 挂在 B 对象上。

实现在 A 脚本的 Start 函数中将 B 对象上的 B 脚本失活(用 GameObject 相关知识做)。

1
2
3
4
5
6
7
8
9
10
11
public class A : MonoBehaviour
{
private void Start()
{
GameObject obj = GameObject.Find("B");
if (obj != null)
{
obj.GetComponent<B>().enabled = false;
}
}
}

第三题

3.一个对象 A 和一个对象 B,在 A 上挂一个脚本,通过这个脚本可以让 B 对象改名,失活,延迟删除,立即删除。可以在 lnspector 窗口进行设置,让 B 实现不同的效果(提示: GameObject、枚举)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public enum E_Do_Type
{
//改名
ChangeName,
//失活
ActiveFlase,
//延迟删除
DelayDes,
//删除
Des,
}

public class Lesson4_A1 : MonoBehaviour
{
//申明一个枚举 表示不同的状态
public E_Do_Type type = E_Do_Type.ChangeName;

//通过Inspector窗口 关联B 这种方式 更加效率
public GameObject B;

// Start is called before the first frame update
void Start()
{
switch (type)
{
case E_Do_Type.ChangeName:
B.name = "B改名";
break;
case E_Do_Type.ActiveFlase:
B.SetActive(false);
break;
case E_Do_Type.DelayDes:
Destroy(B, 5);
break;
case E_Do_Type.Des:
DestroyImmediate(B);
break;
default:
break;
}
}
}

Time 知识点

Time 相关内容主要用来干啥?

时间相关内容 主要 用于游戏中参与位移、记时、时间暂停等

这期主要讲 Time 的一些字段

UnityEngine.Time - Unity 脚本 API

一般来说,单机游戏或者测试时,使用物理时间 Time,网络游戏,使用服务器时间。

image-20230211222931169 image-20230211223129063

知识点一 时间缩放比例

时间停止

1
Time.timeScale = 0;

正常

1
Time.timeScale = 1;

2 倍速

1
Time.timeScale = 2;

知识点二 帧间隔时间

帧间隔时间:Time.deltaTime,最近的一帧用了多长时间(秒),受 scale 影响

1
print("帧间隔时间" + Time.deltaTime);

不受 scale 影响的帧间隔时间:Time.unscaledDeltaTime;

1
print("不受timeScale影响的帧间隔时间" + Time.unscaledDeltaTime);

帧间隔时间,主要是用来计算位移

路程 = 时间 * 速度

根据需求,选择参与计算的间隔时间。

如果希望游戏暂停时就不动的,那就使用deltaTime

如果希望不受暂停影响,使用unscaledDeltaTime

知识点三 游戏开始到现在的时间

Time.time:游戏当前时间(游戏未运行时为 0)

它主要用来计时 单机游戏中计时,受 scale 影响

1
print("游戏开始到现在的时间:" + Time.time);

不受 scale 影响

1
print("不受scale影响的游戏开始到现在的时间:" + Time.unscaledTime);

知识点四 物理帧间隔时间

固定物理帧时间运行的函数为FixedUpdate,这个物理帧时间是可以设置的。

物理帧间隔时间:Time.fixedDeltaTime;

受 scale 影响

1
print(Time.fixedDeltaTime);

不受 scale 影响

1
print(Time.fixedUnscaledDeltaTime);

知识点五 帧数

从开始到现在游戏跑了多少帧(次循环)

1
print(Time.frameCount);

练习题

第一题

在哪里可以设置物理更新的间隔时间?

答:工具栏->Edit->ProjectSettings->Time

第二题

请问 Time 中的各个时间对于我们来说,可以用来做什么?至少选择两个时间变量来进行说明。

UnityEngine.Time - Unity 脚本 API

各种时间加上 unsacled 就是不受 Time.timeScale 影响。

Time.timeScale:时间缩放比例,用来控制时间的快慢。

Time.deltaTime,Time.unscaledDeltaTime:帧时间间隔,可以用来计算行走的路程,路程 = 时间 * 速度。

Time.time:从游戏开始到现在经过的时间,可以用来计时。

Time.fixedDeltaTime:物理帧间隔时间,用来处理物理效果

必不可少的 Transform

UnityEngine.Transform - Unity 脚本 API

Vector3 基础和位置相关 知识点

UnityEngine.Transform - Unity 脚本 API

Transform 主要用来干嘛?

游戏对象(GameObject)位移、旋转、缩放、父子关系、坐标转换等相关操作都由它处理。

它是 Unity 提供的极其重要的类

用于表示 3D 向量和点。

Unity 内部使用该结构传递 3D 位置和方向。 此外,它还包含用于执行常见向量操作的函数。

UnityEngine.Vector3 - Unity 脚本 API

知识点一 必备知识点 Vector3 基础

Vector3 主要是用来表示三维坐标系中的一个点或者一个向量。

声明

1
Vector3 v1 = new Vector3(0, 0, 0);

Vector 的基本计算

1
2
3
4
5
6
7
8
9
//其实就是运算符重载
public static Vector3 operator +(Vector3 a, Vector3 b)
public static Vector3 operator -(Vector3 a, Vector3 b)
public static Vector3 operator -(Vector3 a)
public static Vector3 operator *(Vector3 a, float d)
public static Vector3 operator *(float d, Vector3 a)
public static Vector3 operator /(Vector3 a, float d)
public static bool operator ==(Vector3 lhs, Vector3 rhs)
public static bool operator !=(Vector3 lhs, Vector3 rhs)

常用静态成员,大部分用来表示一个方向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static Vector3 zero => zeroVector;
public static Vector3 one => oneVector;
public static Vector3 forward => forwardVector;
public static Vector3 back => backVector;
public static Vector3 up => upVector;
public static Vector3 down => downVector;
public static Vector3 left => leftVector;
public static Vector3 right => rightVector;
public static Vector3 positiveInfinity => positiveInfinityVector;
public static Vector3 negativeInfinity => negativeInfinityVector;

private static readonly Vector3 zeroVector = new Vector3(0f, 0f, 0f);
private static readonly Vector3 oneVector = new Vector3(1f, 1f, 1f);
private static readonly Vector3 upVector = new Vector3(0f, 1f, 0f);
private static readonly Vector3 downVector = new Vector3(0f, -1f, 0f);
private static readonly Vector3 leftVector = new Vector3(-1f, 0f, 0f);
private static readonly Vector3 rightVector = new Vector3(1f, 0f, 0f);
private static readonly Vector3 forwardVector = new Vector3(0f, 0f, 1f);
private static readonly Vector3 backVector = new Vector3(0f, 0f, -1f);

常用的一个方法

1
2
3
4
5
6
7
8
//返回两点之间距离
public static float Distance(Vector3 a, Vector3 b)
{
float num = a.x - b.x;
float num2 = a.y - b.y;
float num3 = a.z - b.z;
return (float)Math.Sqrt(num * num + num2 * num2 + num3 * num3);
}

知识点二 位置

position 和 localPostion

相对世界坐标系

通过 position 得到的位置,是相对于世界坐标系的原点的位置,可能和面板上显示的是不一样的。

因为如果对象有父子关系,并且父对象位置不在原点,那么和面板上肯定就是不一样的

1
print(this.transform.position);

相对父对象

这两个坐标 对于我们来说很重要,如果你想以面板坐标为准来进行位置设置。

那一定是通过 localPosition 来进行设置的。

1
print(this.transform.localPosition);

position 和 localPositio 可能出现是一样的情况,是因为父对象是原点或者没有父对象。

修改位置

注意:位置的赋值不能直接改变 x,y,z 只能整体改变

因为 position 是结构体,而结构体是值类型,获取 x 时,返回的是一个值副本,这样修改就没啥用啦。

Unity 干脆直接将他设置为不能修改

所以需要使用一个 Vector 中转一下。

在 Unity 中不能对 transform.position.x/y/z 直接赋值的原因探究_自然妙有猫仙人的博客-CSDN 博客_transform.position.x

1
2
3
4
5
6
7
8
9
10
11
12
this.transform.position = new Vector3(10, 10, 10);
this.transform.localPosition = Vector3.up * 10;

//如果只想改一个值x y和z要保持原有坐标一致
//1.直接赋值
this.transform.position = new Vector3(19, this.transform.position.y, this.transform.position.z);
//2.先取出来 再赋值
//虽然不能直接改 transform的 xyz 但是 Vector3是可以直接改 xyz的
//所以可以先取出来改Vector3 再重新赋值
Vector3 vPos = this.transform.localPosition;
vPos.x = 10;
this.transform.localPosition = vPos;

获取以当前物体为参照的其他朝向位置

如果你想得到对象当前的一个朝向 ,那么就是通过 transform.出来的

注意:这个方向是相对于本物体的!!

UnityEngine.Transform - Unity 脚本 API

1
2
3
4
5
6
7
//对象当前的各朝向
//对象当前的面朝向
print(this.transform.forward);
//对象当前的头顶朝向
print(this.transform.up);
//对象当前的右手边
print(this.transform.right);

知识点三 位移

在多维坐标系上,由于存在多个轴,自然就存在方向。

路程 = 方向 _ 速度 _ 时间

方式一,自己计算

用当前的位置 + 我要动多长距离,得出最终所在的位置

移动后的位置 = 当前位置 + 方向 _ 速度 _ 时间

1
2
3
this.transform.position = this.transform.position + this.transform.up * 1 * Time.deltaTime;
//复合运算
this.transform.position += this.transform.forward * 1 * Time.deltaTime;

方式二,调用 API

Transform-Translate - Unity 脚本 API

1
2
3
Transform.Translate;
//6个重载,常用的是下面这个
public void Translate(Vector3 translation, [DefaultValue("Space.Self")] Space relativeTo)

根据 translation 的方向和距离移动变换。

参数一:表示位移多少,路程 = 方向 _ 速度 _ 时间。

参数二:表示相对坐标系默认该参数是相对于自己坐标系的。

1
2
3
4
5
6
7
8
9
10
11
//1相对于世界坐标系的Z轴动,始终是朝世界坐标系的Z轴正方向移动
this.transform.Translate(Vector3.forward * 1 * Time.deltaTime, Space.World);

//2相对于世界坐标的,自己的面朝向去动,始终朝自己的面朝向移动
this.transform.Translate(this.transform.forward * 1 * Time.deltaTime, Space.World);

//3相对于自己的坐标系 下的 自己的面朝向向量移动 (一定不会这样让物体移动) XXXXXXX
this.transform.Translate(this.transform.forward * 1 * Time.deltaTime, Space.Self);

//4相对于自己的坐标系 下的 Z轴正方向移动 始终朝自己的面朝向移动
this.transform.Translate(Vector3.forward * 1 * Time.deltaTime, Space.Self);

注意:一般使用 API 来进行位移

总结

Vector3

如何声明?提供的常用静态属性和一个计算距离的方法是什么?

1
Vector3 v = new Vector3(x, y, z);

提供的常用静态属性:zero,one,forward,left,right,up,down

一个计算距离的方法:Vector.Distanve(v1, v2);

位置(transform.position)

相对于世界坐标系和相对于父对象这两个坐标的区别?

世界坐标:无论物体做了任何的转换(移动,旋转等),世界三个轴方向仍然不变。

本地坐标:
如果物体无父对象,本地坐标就是世界坐标。

如果物体有父对象,本地坐标就是就是相对于父对象的坐标计算出来的坐标(根据父对象的位置进行偏移)。

不能够单独修改 xyz,只能一起统一改?

因为 position 是结构体,而值类型返回的是值副本,所以需要通过中转一个 Vector 修改。

位移(transform.Translate)

自己修改

路程 = 方向 _ 速度 _ 时间

新位置 = 原位置 + 路程向量

调用 API

通常使用 API 进行位移。

1
Translate(Vector3 translation, [DefaultValue("Space.Self")] Space relativeTo)

translation:路程(带方向)

relativeTo:参照坐标系

练习题

第一题

1.一个空对象上挂了一个脚本,这个脚本可以让游戏运行时,在场景中创建出一个 n 层由 Cube 构成的金字塔(提示:实例化预设体或者实例化自带几何体方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void InstantiateGoldenTower(int n = 7)
{
GameObject GoldenTower = new GameObject("GoldenTower");
for(int i = 0; i < n; ++i)
{
for (int k = 0; k < (n - i); ++k)
{
for (int j = 0; j < (n - i); ++j)
{
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube.transform.position = new Vector3((k + 0.5f * i), i, (j + 0.5f * i));
cube.transform.parent = GoldenTower.transform;
}
}
}
}

第二题

1
2
3
4
this.transform.Translate(Vector3.forward, Space.World);
this.transform.Translate(Vector3.forward, Space.Self);
this.transform.Translate(this.transform.forward, Space.Self);
this.transform.Translate(this.transform.forward, Space.World);

这四个方法,哪些才能让对象朝自己的面朝向移动?为何?(可以画图说明)

第二个方法可以,因为是按照本地坐标系,然后朝前移动的。就是按照自己的方向朝自己的前面移动。

第四个方法可以,因为是按照世界坐标系,并且是朝自己前面移动的。

第三题

1
2
this.transform.Translate(Vector3.forward * Time.deltaTime * 1f, Space.Self);
this.transform.Translate(this.transform.forward * Time.deltaTime, Space.World);

角度和旋转 知识点

UnityEngine.Transform - Unity 脚本 API

注意:面板上显示的,都是相对于父对象(local 本地)的坐标或角度。

知识点一 角度相关

Transform-eulerAngles - Unity 脚本 API

相对世界坐标角度

1
2
print(this.transform.eulerAngles);
this.transform.eulerAngles = new Vector3(10, 10, 10);

相对父对象角度

1
2
print(this.transform.localEulerAngles);
this.transform.localEulerAngles = new Vector3(10, 10, 10);

注意:设置角度和设置位置一样,不能单独设置 xyz 要一起设置。

如果我们希望改变的角度是面板上显示的内容,那一定是改变相对父对象(本地)的角度。

知识点二 旋转相关

自己计算(省略不讲了 和位置一样 不停改变角度即可)

API

自转

每个轴,具体转多少度

Transform.Rotate

使用 Transform.Rotate 以各种方式旋转 GameObjects。通常以欧拉角而不是四元数提供旋转。

可以在世界轴或本地轴中指定旋转。

Transform-Rotate - Unity 脚本 API

第一个参数 相当于 是旋转的角度 每一帧
第二个参数 默认不填 就是相对于自己坐标系 进行的旋转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//每个轴 具体转多少度
//第一个参数 相当于 是旋转的角度 每一帧
//第二个参数 默认不填 就是相对于自己坐标系 进行的旋转
this.transform.Rotate(new Vector3(0, 10, 0) * Time.deltaTime);
this.transform.Rotate(new Vector3(0, 10, 0) * Time.deltaTime, Space.World);

//相对于某个轴 转多少度
//参数一:是相对哪个轴进行转动
//参数二:是转动的 角度 是多少
//参数三:默认不填 就是相对于自己的坐标系 进行旋转
// 如果填 可以填写相对于 世界坐标系进行旋转
this.transform.Rotate(Vector3.right, 10 * Time.deltaTime);
this.transform.Rotate(Vector3.right, 10 * Time.deltaTime, Space.World);

//相对于某一个点转
//参数一:相当于哪一个点 转圈圈
//参数二:相对于那一个点的 哪一个轴转圈圈
//参数三:转的度数 旋转速度 * 时间
this.transform.RotateAround(Vector3.zero, Vector3.right, 10 * Time.deltaTime);

练习题

第一题

1.使用你之前创建的坦克预设体,在坦克下面加一个底座(用自带几何体即可)让其可以原地旋转,类似一个展览台

将坦克作为展示盘的子对象,然后围绕自身 y 轴旋转。

1
this.transform.Rotate(Vector3.up * 10 * Time.deltaTime);

第二题

2.在第一题的基础上,让坦克的炮台可以自动左右来回旋转,炮管可以自动上下抬起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//**********************************
//创建人:
//功能说明:
//**********************************
public class Lesson7_P : MonoBehaviour
{
public GameObject tankBattery;
public GameObject tankGunBarrel;
int upDownFlag = 1;
int leftRightFlag = 1;
// Start is called before the first frame update
void Start()
{
tankBattery = GameObject.Find("炮台");
tankGunBarrel = GameObject.Find("炮管");
}

// Update is called once per frame
void Update()
{
//整体旋转
this.transform.Rotate(Vector3.up * 10 * Time.deltaTime);
//炮台左右旋转
tankBattery?.transform.Rotate(Vector3.up * 10 * leftRightFlag * Time.deltaTime);
if (tankBattery?.transform.eulerAngles.y > 180f || tankBattery?.transform.eulerAngles.y < -180f)
{
leftRightFlag = -leftRightFlag;
}

//炮管上下旋转
tankGunBarrel?.transform.Rotate(Vector3.right * 10 * upDownFlag * Time.deltaTime);
if (tankGunBarrel?.transform.eulerAngles.x > 16f || tankGunBarrel?.transform.eulerAngles.x < -20f)
{
upDownFlag = -upDownFlag;
}
}
}

注意,localEulerAngles 是没有负数的!!

第三题

3.请用 3 个球体,模拟太阳、地球、月亮之间的旋转移动

写的不太对,不要看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//**********************************
//创建人:
//功能说明:
//**********************************
public class Lesson7_P3 : MonoBehaviour
{
public GameObject sun;
public GameObject earth;
public GameObject moon;
// Start is called before the first frame update
void Start()
{
sun = GameObject.Find("sun");
earth = GameObject.Find("earth");
moon = GameObject.Find("moon");
}

// Update is called once per frame
void Update()
{
sun.transform.Rotate(Vector3.up * 30 * Time.deltaTime, Space.Self);
earth.transform.RotateAround(sun.transform.position, Vector3.up, 20 * Time.deltaTime);
moon.transform.RotateAround(earth.transform.position, Vector3.up, 10 * Time.deltaTime);
}
}

答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson7_Exercises : MonoBehaviour
{
public float rotateSpeed = 10;
public float headRotateSpeed = 10;
public float pkPosRotateSpeed = 10;
//头部位置信息
public Transform head;
public Transform pkPos;

public Transform dq;
public Transform ty;
public Transform yl;
// Start is called before the first frame update
void Start()
{
#region 练习题一
//1.使用你之前创建的坦克预设体,在坦克下面加一个底座(用自带几何体即可)
//让其可以原地旋转,类似一个展览台
#endregion

#region 练习题二
//2.在第一题的基础上,让坦克的炮台可以自动左右来回旋转,炮管可以自动上下抬起
#endregion

#region 练习题三
//3.请用3个球体,模拟太阳、地球、月亮之间的旋转移动
#endregion
}

// Update is called once per frame
void Update()
{
#region 练习题一
this.transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime);
#endregion

#region 练习题二
//炮台左右来回旋转
head.Rotate(Vector3.up, headRotateSpeed * Time.deltaTime);
//炮管上下来回旋转
pkPos.Rotate(Vector3.right, pkPosRotateSpeed * Time.deltaTime);
//通过head.localEulerAngles得到的角度 不会出现负数的情况
//虽然界面上显示出了负数 但是 通过代码获取 始终 只能得到0~360之间的数

//只能是0到360 那就只有特殊判断了
if (!(head.localEulerAngles.y >= 315 && head.localEulerAngles.y <= 360) &&
head.localEulerAngles.y >= 45 && headRotateSpeed > 0)
headRotateSpeed = -headRotateSpeed;
else if (!(head.localEulerAngles.y <= 45 && head.localEulerAngles.y >= 0) &&
head.localEulerAngles.y <= 315 && headRotateSpeed < 0)
headRotateSpeed = -headRotateSpeed;

//只能是0到360 那就只有特殊判断了
if (!(pkPos.localEulerAngles.x >= 350 && pkPos.localEulerAngles.x <= 360) &&
pkPos.localEulerAngles.x >= 10 && pkPosRotateSpeed > 0)
pkPosRotateSpeed = -pkPosRotateSpeed;
else if (!(pkPos.localEulerAngles.x <= 10 && pkPos.localEulerAngles.x >= 0) &&
pkPos.localEulerAngles.x <= 350 && pkPosRotateSpeed < 0)
pkPosRotateSpeed = -pkPosRotateSpeed;
#endregion

#region 练习题三
//太阳自转
ty.Rotate(Vector3.up, 10 * Time.deltaTime);
//地球自转
dq.Rotate(Vector3.up, 10 * Time.deltaTime);
//月亮自转
yl.Rotate(Vector3.up, 10 * Time.deltaTime);

//地球公转
dq.RotateAround(ty.position, Vector3.up, 10 * Time.deltaTime);
#endregion
}
}

缩放和看向 知识点

UnityEngine.Transform - Unity 脚本 API

知识点一 缩放

注意:lossyScale 不能修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//相对世界坐标系
print(this.transform.lossyScale);
//相对本地坐标系(父对象)
print(this.transform.localScale);

//注意:
//1.同样缩放不能只改xyz 只能一起改(相对于世界坐标系的缩放大小只能得 不能改)
//所以 我们一般要修改缩放大小 都是改的 相对于父对象的 缩放大小 localScale
this.transform.localScale = new Vector3(3, 3, 3);

//2.Unity没有提供关于缩放的API
//之前的 旋转 位移 都提供了 对应的 API 但是 缩放并没有
//如果你想要 让 缩放 发生变化 只能自己去写(自己算)
this.transform.localScale += Vector3.one * Time.deltaTime;

知识点二 看向

Transform-LookAt - Unity 脚本 API

让一个对象的面朝向,可以一直看向某一个点或者某一个对象。

看向一个点,相对于世界坐标系的。

1
this.transform.LookAt(Vector3.zero);

看向一个对象就传入一个对象的 Transform 信息。

1
this.transform.LookAt(lookAtObj);

总结

缩放相关:

相对于世界坐标系的缩放,只能得,不能改,只能去修改相对于本地坐标系的缩放(相对于父对象)。

没有提供对应的 API 来 缩放变化 只能自己算

看向:LookAt 看向一个点 或者一个对象。一定记住,是写在 Update 里面才会不停变化。

练习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//**********************************
//创建人:
//功能说明:
//**********************************
public class Lesson8_p : MonoBehaviour
{
public GameObject camera;

// Start is called before the first frame update
void Start()
{
camera = GameObject.Find("Main Camera");

}

// Update is called once per frame
void LateUpdate()
{
camera.transform.LookAt(GameObject.Find("展示台").transform);
}
}

父子关系 知识点

UnityEngine.Transform - Unity 脚本 API

知识点一 获取和设置父对象

获取父对象

1
print(this.transform.parent.name);

设置父对象,断绝父子关系

1
this.transform.parent = null;

设置父对象 认爸爸

1
this.transform.parent = GameObject.Find("Father2").transform;

通过 API 来进行父子关系的设置

1
2
this.transform.SetParent(null);//断绝父子关系
this.transform.SetParent(GameObject.Find("Father2").transform);//认爸爸

重载

参数一:目标父对象。
参数二:是否保留世界坐标的 位置 角度 缩放 信息。

true:会保留世界坐标下的状态和父对象进行计算,得到本地坐标系的信息,原来缩放怎么样,还是怎么样。

false:不会保留会直接把世界坐标系下的位置角度缩放,直接赋值到本地坐标系下,会参照父对象进行坐标缩放变换。

1
this.transform.SetParent(GameObject.Find("Father3").transform, false);

知识点二 抛妻弃子

就是和自己的所有儿子,断绝关系,没有父子关系了。

1
this.transform.DetachChildren();

知识点三 获取子对象

transform.Find

按名字查找儿子,找到儿子的 transform 信息

transform.Find方法,是能够找到失活的对象的 !!!!!

GameObject 相关的 查找 是不能找到失活对象的

1
print(this.transform.Find("Cube (1)").name);

他只能找到自己的儿子 找不到自己的孙子 !!!!!!

1
2
//GameObject是Cube (1)的子对象
print(this.transform.Find("GameObject").name);

虽然它的效率比 GameObject.Find 要高一些,但是前提是你必须知道父亲是谁才能找。

遍历儿子

先获取子对象数量

  1. 失活的儿子也会算数量
  2. 找不到孙子所以孙子不会算数量
1
2
//这是一个字段
print(this.transform.childCount);

再通过 transform.GetChild(index)获取。

通过索引号,去得到自己对应的儿子。如果编号超出了儿子数量的范围,会直接报错。

返回值是transform,可以得到对应儿子的位置相关信息。

1
2
3
4
5
6
this.transform.GetChild(0);

for (int i = 0; i < this.transform.childCount; i++)
{
print("儿子的名字:" + this.transform.GetChild(i).name);
}

知识点四 儿子的操作

一个对象,判断自己是不是另一个对象的儿子。

1
2
3
4
if(son.IsChildOf(this.transform))
{
print("是我的儿子");
}

得到自己作为儿子的编号,编号从 0 开始

1
print(son.GetSiblingIndex());

把自己设置为第一个儿子

1
son.SetAsFirstSibling();

把自己设置为最后一个儿子

1
son.SetAsLastSibling();

把自己设置为指定个儿子

就算你填的数量 超出了范围(负数或者更大的数) 不会报错 会直接设置成最后一个编号

1
son.SetSiblingIndex(1);

总结

设置父对象相关的内容

1
transform.SetParent(obj, true/false);

获取子对象

1
transform.childCount;

抛弃妻子

1
transform.DetachChildren();

儿子的操作

1
2
3
4
5
transform.IsChildOf(obj);
transform.GetSiblingIndex();
transform.SetSiblingIndex(int);
transform.SetAsFirstSibling();
transform.SetAsLastSibling();

练习题

第一题

请为 Transform 写一个拓展方法,可以将它的子对象按名字的长短进行排序改变他们的顺序,名字短的在前面,名字长的在后面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static class Lesson9_P
{
public static void SortByName(this Transform transform)
{
//这里可以使用List
Transform[] trans = new Transform[transform.childCount];
for (int i = 0; i < transform.childCount; ++i)
{
trans[i] = transform.GetChild(i);
}
Array.Sort(trans, (a, b) => a.name.Length < b.name.Length ? -1 : 1);
for (int i = 0; i < trans.Length; i++)
{
trans[i].SetSiblingIndex(i);
}
}
}

第二题

请为 Transform 写一个拓展方法,传入一个名字查找子对象,即使是子对象的子对象也能查找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Transform FindInAllSubObject(this Transform parent, string name)
{
Transform ret = parent.Find(name);
if (ret == null)
{
for (int i = 0; i < parent.childCount; ++i)
{
ret = parent.GetChild(i).FindInAllSubObject(name);
if (ret != null)
{
return ret;
}
}
}
return ret;
}

答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public static class Tools
{
/// <summary>
/// 这个方法 是我们为Transform写的一个拓展方法
/// 主要用来把子对象按名字长短排序
/// </summary>
/// <param name="obj"></param>
public static void Sort( this Transform obj)
{
//有什么知识点 是和排序相关
//使用List的排序 比较符合我们的需求
//我们的思路 就是把子对象 放到一个 list容器中 然后对list进行排序
//排序完毕后 遍历list 去设置 这个顺序中的 对象的 子对象位置
List<Transform> list = new List<Transform>();
for (int i = 0; i < obj.childCount; i++)
{
list.Add(obj.GetChild(i));
}
//这是根据 名字长短进行排序 利用的 是list的排序
list.Sort((a, b) =>
{
if (a.name.Length < b.name.Length)
return -1;
else
return 1;
});
//根据 list中的排序结果 重新设置每一个对象的 索引编号
for (int i = 0; i < list.Count; i++)
{
list[i].SetSiblingIndex(i);
}
}


/// <summary>
/// 根据名字找到 子对象 不管藏多深
/// </summary>
/// <param name="father">调用方法的对象</param>
/// <param name="childName">要找的对象名字</param>
/// <returns></returns>
public static Transform CustomFind( this Transform father, string childName)
{
//如何查找子对象的子对象
//我要找的对象
Transform target = null;
//先从自己身上的子对象找
target = father.Find(childName);
if (target != null)
return target;

//如果在自己身上没有找到 那就去找自己的子对象的子对象
for (int i = 0; i < father.childCount; i++)
{
//让子对象去帮我找 有没有这个名字的对象
//递归
target = father.GetChild(i).CustomFind(childName);
//找到了 直接返回
if (target != null)
return target;
}

return target;
}
}

坐标转换 知识点

世界坐标系指向是永远不变的,而本地坐标系指向是会随着自身旋转而改变的。

UnityEngine.Transform - Unity 脚本 API

知识点一 世界坐标转本地坐标

世界坐标系转本地坐标系,可以帮助我们大概判断一个相对位置。

将世界坐标系的点转换为相对本地坐标系的点。

这个相对于本地坐标系是指:以当前物体坐标为中心

比如世界坐标(0,0,0)相对于坐标(-5, 0, 5)的本地坐标,就是(0,0,0)->(-5, 0, -5)

就是以(-5, 0, 5)为中心(0,0,0),当前坐标相对于(-5, 0, 5)的坐标是什么。

受到缩放影响(会除以缩放值)

transform.InverseTransformPoint:世界坐标系的,转换为相对本地坐标系的(受到缩放影响)

transform.InverseTransformVector:世界坐标系的方向,转换为相对本地坐标系的方向 (受到缩放影响)

transform.InverseTransformDirection:世界坐标系的方向,转换为相对本地坐标系的方向 (不受缩放影响)

1
2
3
//受缩放影响
print("转换后的点 " + this.transform.InverseTransformPoint(Vector3.forward));
print("转换后的方向(受缩放影响)" + this.transform.InverseTransformVector(Vector3.forward));
1
2
3
//世界坐标系的方向 转换 为相对本地坐标系的方向
//不受缩放影响
print("转换后的方向" + this.transform.InverseTransformDirection(Vector3.forward));

知识点二 本地坐标转世界坐标

常用,比如我们需要在我们的前面释放一个技能,需要释放到世界坐标。

那么,我们就直接将我们坐标的前面的坐标,转换成世界坐标就可以了。

transform.TransformPoint:本地坐标系的,转换为相对世界坐标系的 (受到缩放影响)

transform.TransformDirection:本地坐标系的方向,转换为相对世界坐标系的方向(受到缩放影响)

transform.TransformVector:本地坐标系的方向,转换为相对世界坐标系的方向(不受缩放影响)

1
2
3
4
5
6
7
8
//本地坐标系的点 转换 为相对世界坐标系的点 受到缩放影响
print("本地 转 世界 点" + this.transform.TransformPoint(Vector3.forward));

//本地坐标系的方向 转换 为相对世界坐标系的方向
//不受缩放影响
print("本地 转 世界 方向" + this.transform.TransformDirection(Vector3.forward));
//受缩放影响
print("本地 转 世界 方向" + this.transform.TransformVector(Vector3.forward));

练习题

第一题

一个物体 A,不管它在什么位置,写一个方法,只要执行这个方法就可以在它的左前方(-1,0,1),处创建一个空物体

1
2
3
4
5
public void CreateObj()
{
GameObject obj = new GameObject("左前方的物体");
obj.transform.position = this.transform.TransformPoint(new Vector3(-1, 0, 1));
}

第二题

一个物体 A,不管它在什么位置,写一个方法,只要执行这个方法就可以在它的前方创建出 3 个球体,位置分别是(0,0,1),(0,0,2),(0,0,3)

1
2
3
4
5
6
7
8
public void CreateSphere()
{
for (int i = 0; i < 3; ++i)
{
GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Sphere);
obj.transform.position = this.transform.TransformPoint(new Vector3(0, 0, i + 1));
}
}

答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson10_Exercises : MonoBehaviour
{
[ContextMenu("左前方创建空物体")]
void TestFun1()
{
#region 练习题一
//一个物体A,不管它在什么位置,写一个方法,只要执行这个方法就可以在它的左前方(-1,0,1)处创建一个空物体
//Vector3 pos = this.transform.TransformPoint(new Vector3(-1, 0, 1));
GameObject obj = new GameObject("左前方物体");
obj.transform.position = this.transform.TransformPoint(new Vector3(-1, 0, 1));
#endregion
}

[ContextMenu("面前创建3个球体")]
void TestFun2()
{
#region 练习题二
//一个物体A,不管它在什么位置,写一个方法,只要执行这个方法就可以在它的前方创建出3个球体,
//位置分别是(0,0,1),(0,0,2),(0,0,3)
//GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Sphere);
//obj.transform.position = this.transform.TransformPoint(new Vector3(0, 0, 1));

//obj = GameObject.CreatePrimitive(PrimitiveType.Sphere);
//obj.transform.position = this.transform.TransformPoint(new Vector3(0, 0, 2));

//obj = GameObject.CreatePrimitive(PrimitiveType.Sphere);
//obj.transform.position = this.transform.TransformPoint(new Vector3(0, 0, 3));

for (int i = 1; i <= 3; i++)
{
GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Sphere);
obj.transform.position = this.transform.TransformPoint(Vector3.forward * i);
}
#endregion
}
}

Input 鼠标键盘输入 知识点

注意:输入相关内容肯定是写在 Update 中的

UnityEngine.Input - Unity 脚本 API

知识点一 鼠标在屏幕位置

屏幕坐标的原点是在屏幕的左下角,往右是 X 轴正方向,往上时 Y 轴正方向。

返回值时 Vector3,但是只有 x 和 y 有值。z 一直是 0,是因为屏幕本来就是 2D 的 不存在 Z 轴。

知识点二 检测鼠标输入

鼠标按下相关检测,对于我们来说

比如: 1.可以做发射子弹;2.可以控制摄像机 转动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//鼠标按下一瞬间 进入
//0左键 1右键 2中键
//只要按下的这一瞬间 进入一次
if( Input.GetMouseButtonDown(0) )
{
print("鼠标左键按下了");
}

//鼠标抬起一瞬间 进入
if( Input.GetMouseButtonUp(0) )
{
print("鼠标左键抬起了");
}

//鼠标长按按下抬起都会进入
//就是 当按住按键不放时 会一直进入 这个判断
if( Input.GetMouseButton(1))
{
print("右键按下");
}

//中键滚动
//返回值的 y -1往下滚 0没有滚 1往上滚
//它的返回值 是Vector的值 我们鼠标中键滚动 会改变其中的Y值
print(Input.mouseScrollDelta);

知识点三 检测键盘输入

注意:如果使用字符串重载,则只能传入小写字母字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//比如说 按一个键释放一个技能或者切换武器 等等的操作

//键盘按下
if( Input.GetKeyDown(KeyCode.W) )
{
print("W键按下");
}

//传入字符串的重载
//这里传入的 字符串 不能是大写的 不然会报错
//只能传入小写字符串
if( Input.GetKeyDown("q") )
{
print("q按下");
}

//键盘抬起
if( Input.GetKeyUp(KeyCode.W) )
{
print("W键抬起");
}

//键盘长按
if( Input.GetKey(KeyCode.W) )
{
print("W键长按");
}

知识点四 检测默认轴输入

我们学习鼠标、键盘输入,主要是用来控制玩家,比如旋转、位移等等。

所以 Unity 提供了更方便的方法,来帮助我们控制对象的位移和旋转。

Input.GetAxis(string)Input.GetAxisRaw(string)

注意:这个参数,是可以设置的,在 Edit->ProjectSettings->InputManager

GetAxisRaw 的返回值是-1 或 1 或 0。

GeAxisRaw 的返回值就是一个在【-1,1】精确的 float 值。

参数:

Horizontal:水平移动,检测 A,D

Vertical:垂直移动,检测 W,S

Mouse X:鼠标水平移动

Mouse Y:鼠标垂直移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//键盘AD按下时 返回 -1到1之间的变换
//相当于 得到得这个值 就是我们的 左右方向 我们可以通过它来控制 对象左右移动 或者左右旋转
float h = Input.GetAxis("Horizontal");
print(h);

//键盘SW按下时 返回 -1到1之间的变换
//得到得这个值 就是我们的 上下方向 我们可以通过它来控制 对象上下移动 或者上下旋转
print(Input.GetAxis("Vertical"));

//鼠标横向移动时 -1 到 1 左 右
print(Input.GetAxis("Mouse X"));

//鼠标竖向移动时 -1 到 1 下 上
print(Input.GetAxis("Mouse Y"));

//我们默认的 GetAxis方法 是有渐变的 会总 -1~0~1之间 渐变 会出现小数

//GetAxisRaw方法 和 GetAxis使用方式相同
//只不过 它的返回值 只会是 -1 0 1 不会有中间值

Input 触摸手柄陀螺仪 知识点

其它

是否有任意键或鼠标长按

1
if (Input.anyKey) ...

是否有任意键或鼠标按下

1
if (Input.anyKeyDown) ...

获取当前帧输入的是什么按键

1
2
//这一帧的键盘输入
print(Input.inputString);

手柄输入相关

得到连接的手柄的所有按钮名字

1
string[] strs = Input.GetJoystickNames();

某一个手柄键按下

1
if(Input.GetButtonDown("Jump"))

某一个手柄键抬起

1
if(Input.GetButton("Jump"))

移动设备触摸相关

UnityEngine.Touch - Unity 脚本 API

触摸次数。保证在整个帧期间不会更改。(只读)

1
2
3
4
5
6
7
8
if(Input.touchCount > 0)
{
Touch t1 = Input.touches[0];
//位置
print(t1.position);
//相对上次位置的变化
print(t1.deltaPosition);
}

是否启用多点触控

该属性指示系统是否处理多点触控。

1
Input.multiTouchEnabled = false;

陀螺仪(重力感应)

是否开启陀螺仪 必须开启 才能正常使用

1
Input.gyro.enabled = true;

重力加速度向量

1
print(Input.gyro.gravity);

旋转速度

1
print(Input.gyro.rotationRate);

陀螺仪当前的旋转四元数

比如,可以用这个角度信息来控制场景上的一个 3D 物体受到重力影响

手机怎么动它怎么动

1
print(Input.gyro.attitude);

总结

Input 类提供大部分和输入相关的内容:鼠标、键盘、触屏、手柄、重力感应。

对于我们目前来说,鼠标、键盘是必须掌握的核心知识。

今天必须记住鼠标键盘输入相关的 API,GetAxis 等等。

练习题

把这道题的代码保留好,之后的题会用到。

第一题

1.使用之前的坦克预设体,用 WASD 键控制坦克的前景后退,左右转向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//**********************************
//创建人:
//功能说明:
//**********************************
public class Lesson11_P : MonoBehaviour
{
float horizontal = 0;
float vertical = 0;
// Update is called once per frame
void Update()
{
horizontal = Input.GetAxis("Horizontal");
vertical = Input.GetAxis("Vertical");
this.transform.Rotate(Vector3.up * 50 * horizontal * Time.deltaTime);
this.transform.Translate(Vector3.forward * 5 * vertical* Time.deltaTime, Space.Self);
}
}

第二题

2.在上一题的基础上,鼠标左右移动控制炮台的转向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//**********************************
//创建人:
//功能说明:
//**********************************
public class Lesson11_P : MonoBehaviour
{
float horizontal = 0;
float vertical = 0;
float mouseHorizontal = 0;

float moveSpeed = 5;
float roateSpeed = 50;
Transform tankBattery;
// Start is called before the first frame update
void Start()
{
tankBattery = this.transform.Find("炮台");

}

// Update is called once per frame
void Update()
{
horizontal = Input.GetAxis("Horizontal");
vertical = Input.GetAxis("Vertical");
this.transform.Rotate(Vector3.up * roateSpeed * horizontal * Time.deltaTime);
this.transform.Translate(Vector3.forward * moveSpeed * vertical* Time.deltaTime, Space.Self);

mouseHorizontal = Input.GetAxis("Mouse X");
tankBattery.Rotate(Vector3.up * roateSpeed * mouseHorizontal * Time.deltaTime);
}
}

屏幕相关 Screen 知识点

UnityEngine.Screen - Unity 脚本 API

知识点一 静态属性

常用

当前屏幕设备分辨率

1
2
Resolution r = Screen.currentResolution;
print("当前屏幕分辨率的宽" + r.width + "高" + r.height);

屏幕窗口当前宽高

这得到的是当前窗口的宽高,不是设备分辨率的宽高。

一般写代码,要用窗口宽高做计算时,就用他们。

1
2
print(Screen.width);
print(Screen.height);

屏幕休眠模式

节能设置,允许屏幕在无用户交互一段时间后变暗。

1
Screen.sleepTimeout = SleepTimeout.NeverSleep;

不常用

运行时是否全屏模式

1
Screen.fullScreen = true;

窗口模式:

独占全屏:FullScreenMode.ExclusiveFullScreen

全屏窗口:FullScreenMode.FullScreenWindow

最大化窗口:FullScreenMode.MaximizedWindow

窗口模式:FullScreenMode.Windowed

1
Screen.fullScreenMode = FullScreenMode.Windowed;

移动设备屏幕转向相关

1
2
3
4
5
6
7
8
9
10
11
12
//移动设备屏幕转向相关
//允许自动旋转为左横向 Home键在左
Screen.autorotateToLandscapeLeft = true;
//允许自动旋转为右横向 Home键在右
Screen.autorotateToLandscapeRight = true;
//允许自动旋转到纵向 Home键在下
Screen.autorotateToPortrait = true;
//允许自动旋转到纵向倒着看 Home键在上
Screen.autorotateToPortraitUpsideDown = true;

//指定屏幕显示方向
Screen.orientation = ScreenOrientation.Landscape;

练习题

1.在输入习题的基础上,鼠标滚轮控制控制炮管的抬起放下

2.在上一题的基础上,加入长按鼠标右键移动鼠标可以让摄像机围着坦克旋转,改变观察坦克的视角

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//**********************************
//创建人:
//功能说明:
//**********************************
public class Lesson12_P : MonoBehaviour
{
float horizontal = 0;
float vertical = 0;
float mouseHorizontal = 0;
float mouseVertical = 0;

float moveSpeed = 5;
float rotateSpeed = 50;
Transform tankBattery;
Transform tankGunBarrel;
Transform camera;
// Start is called before the first frame update
void Start()
{
tankBattery = this.transform.Find("炮台");
tankGunBarrel = tankBattery.Find("炮管");
camera = this.transform.Find("Main Camera");
camera.LookAt(this.transform);

}

// Update is called once per frame
void Update()
{
horizontal = Input.GetAxis("Horizontal");
vertical = Input.GetAxis("Vertical");
this.transform.Rotate(Vector3.up * rotateSpeed * horizontal * Time.deltaTime);
this.transform.Translate(Vector3.forward * moveSpeed * vertical * Time.deltaTime, Space.Self);

mouseHorizontal = Input.GetAxis("Mouse X");
mouseVertical = Input.GetAxis("Mouse Y");
tankBattery.Rotate(Vector3.up * rotateSpeed * mouseHorizontal * Time.deltaTime);

tankGunBarrel.Rotate(Vector3.left * rotateSpeed * Input.mouseScrollDelta * Time.deltaTime);

if (Input.GetMouseButton(1))
{
camera.RotateAround(this.transform.position, Vector3.up, rotateSpeed * mouseHorizontal * Time.deltaTime);
}
}
}

答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson11_Exercises : MonoBehaviour
{
public float moveSpeed = 10;
public float rotateSpeed = 10;

public Transform head;
public float headRotateSpeed = 50;

public Transform pkPos;
public float pkRotateSpeed = 20;

//子弹预设体
public GameObject bulletObj;
//子弹将要出现的位置
public Transform bulletPos;
// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame
void Update()
{
#region 练习题一
//1.使用之前的坦克预设体,用WASD键控制坦克的前景后退,左右转向
//Transform当中的 位移 自转 相关知识点
//键盘输入相关知识点

//Input.GetAxis("Horizontal"); 水平方向 -1到1之间的值 0就是没有按下
//Input.GetAxis("Vertical"); 竖直方向 -1到1之间的值 0就是没有按下
//ws键 控制位移
// 这公式 是 : 前进方向 * 速度 * 时间 * 当前是否移动(-1~1 相当于 正向还是反向的感觉 不按就不动 0)
this.transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime * Input.GetAxis("Vertical"));

//ad键 控制 左右转向
// 这公式 是 : 转动的轴 * 速度 * 时间 * 当前是否移动(-1~1 相当于 正向还是反向的感觉 不按就不动 0)
this.transform.Rotate(Vector3.up * rotateSpeed * Time.deltaTime * Input.GetAxis("Horizontal"));
#endregion

#region 练习题二
//2.在上一题的基础上,鼠标左右移动控制炮台的转向
//鼠标输入相关
//Input.GetAxis("Mouse X");
// 这公式 是 : 转动的轴 * 速度 * 时间 * 当前是否移动(-1~1 相当于 正向还是反向的感觉 不按就不动 0)
head.Rotate(Vector3.up * headRotateSpeed * Time.deltaTime * Input.GetAxis("Mouse X"));
#endregion

#region Lesson12 练习题一
//1.在输入习题的基础上,鼠标滚轮控制控制炮管的抬起放下
//Input.mouseScrollDelta.y
pkPos.Rotate(Vector3.right * pkRotateSpeed * Time.deltaTime * Input.mouseScrollDelta.y);
#endregion

#region Lesson16 练习题一
if( Input.GetMouseButtonDown(0) )
{
//实例化一个子弹对象
GameObject obj = Instantiate(bulletObj);
//设置对象的位置
obj.transform.position = bulletPos.position;
//角度
//obj.transform.rotation = bulletPos.rotation;
//角度
obj.transform.eulerAngles = bulletPos.eulerAngles;
}
#endregion
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson12_Exercises : MonoBehaviour
{
//摄像机看向的对象
public Transform target;
//摄像机旋转的速度
public float roundSpeed = 50;

// Start is called before the first frame update
void Start()
{
#region 练习题一
//1.在输入习题的基础上,鼠标滚轮控制控制炮管的抬起放下
#endregion

#region 练习题二
//2.在上一题的基础上,加入长按鼠标右键移动鼠标
// 可以让摄像机围着坦克旋转,改变观察坦克的视角
#endregion
}

// Update is called once per frame
void Update()
{
this.transform.LookAt(target);

//使用 绕着某一个点 的某一个轴 旋转的知识点 进行处理
//鼠标 右键按下 知识点
if( Input.GetMouseButton(1) )
{
this.transform.RotateAround(target.position, //围绕的点
Vector3.up, //哪个轴
roundSpeed * Time.deltaTime * Input.GetAxis("Mouse X"));//速度 Mouse X -1到1
}
}
}

Camera 可编辑参数 知识点

Camera component - Unity 手册

常用重要的

Clear Flags

设置如何清除背景。

确定将清除屏幕的哪些部分。使用多个摄像机来绘制不同游戏元素时,这会很方便

SkyBox:天空盒,主要用于 3D 游戏。

SolidColor:颜色填充,一般用于 2D 游戏。

Depth only:叠加渲染,只渲染当前 Depth 上的物体,配合 Depth 使用。

Dont’t clear:不移除上一帧的渲染,一般不使用。

Background

背景颜色,配合 SolidColor 使用。

Culling Mask

包含或忽略要由摄像机渲染的对象层。在检视面板中将层分配到对象。

确定需要渲染的层级。

Projection

切换摄像机模拟透视的功能。

Perspective:摄像机将以完整透视角度渲染对象。

Orthographic:摄像机将均匀渲染对象,没有透视感。

注意:在正交模式下不支持延迟渲染。始终使用前向渲染。

FOV Axis

摄像机视口轴。

Field of view

根据轴进行大小调整。

Size

正交视口大小。

Clipping Planes

裁剪屏幕距离,在 near-far 区间内,才能被看到,如果不在这个区间,将被裁剪。

Near:最近距离

Far:最远距离

Depth

渲染顺序上的深度。

数字越小越先被渲染,越大越后被渲染。

TargetTexture

渲染纹理

可以把摄像机画面渲染到—张图,上主要用于制作小地图

在 Project 右键创建 Render Texture

Occlusion Culling

是否启用剔除遮挡,一般默认勾选。

是否渲染看不见的物体(比如一个物体在另一个物体后面,看不到)

其他

Viewport Rect

视口范围,屏幕上将绘制该摄像机视图的位置。

主要用于双摄像机游戏,0~1 相当于宽高百分比。

Rendering Path

渲染路径,不常用,了解即可。

HDR

是否允许高动态范围渲染

MSAA

是否允许抗锯齿

Dynamic Resolution

是否允许动态分辨率呈现

Target Display

用于哪个显示器,主要用来开发有多个屏幕的平台游戏。

练习题

1.请用两个摄像机实现分屏效果

用之前制作的可以移动的坦克,一个摄像机俯视坦克跟随移动一个摄像机在炮口位置跟随坦克炮口移动。

2.场景上有两个物体 A 和 B,有两个摄像机 A 和 B。A 摄像机渲染 A,B 摄像机渲染 B,玩家能在 Game 窗口同时看到 A 和 B

Camera 代码相关 知识点

UnityEngine.Camera - Unity 脚本 API

知识点一 重要静态成员

注意:主摄像机的 tag 一定是 MainCamera,不然 MainCamera 回事空的

1.获取摄像机

如果用之前的知识,来获取摄像机,需要使用 Find 或者对象关联。

Unity 提供了直接获取的方法。

如果想通过这种方式,快速获取主摄像机,那么场景上必须有一个 tag 为 MainCamera 的摄像机。

如果有多个主摄像机,则获取第一个。一般来说,只有一个主摄像机。

1
2
//主摄像机的获取
print(Camera.main.name);

获取摄像机的数量

1
print(Camera.allCamerasCount);

得到所有摄像机

1
2
Camera[] allCamera = Camera.allCameras;
print(allCamera.Length);

2.渲染相关委托

摄像机剔除前处理的委托函数

1
2
3
4
5
//参数是一个Camera
Camera.onPreCull += (c) =>
{
...
};

摄像机渲染后处理的委托

1
2
3
4
5
//参数是一个Camera
Camera.onPoseCull _= (c)
{

};

知识点二 重要成员

1.界面上的参数 都可以在 Camera 中获取到

比如 下面这句代码 就是得到主摄像机对象 上的深度 进行设置

1
Camera.main.depth = 10;

2.世界坐标转屏幕坐标

将一个世界坐标,转换为屏幕坐标

我们会用这个来做的功能,最多的就是头顶血条相关的功能

转换过后,x 和 y 对应的就是屏幕坐标,z 对应的 是 这个 3D 物体 里我们的摄像机有多远。

1
2
Vector3 v = Camera.main.WorldToScreenPoint(this.transform.position);
print(v);

3.屏幕坐标转世界坐标

将一个屏幕坐标,转换为世界坐标

之所以改变 Z 轴,是因为,如果不改 Z 默认为 0。

转换过去的世界坐标系的点,永远都是一个点,可以理解为,视口相交的焦点。

如果改变了 Z,那么转换过去的世界坐标的点,就是相对于摄像机前方多少的单位的横截面上的世界坐标点。

这个 z 就是摄像机的 z 离转换后 z 坐标的距离

1
2
3
4
//不断获取鼠标位置,然后重置物体位置。
Vector3 v = Input.mousePosition;
v.z = 5;
obj.position = Camera.main.ScreenToWorldPoint(v);

练习题

第一题

1.游戏画面中央有一个立方体,请将该立方体的世界坐标系位置,转换为屏幕坐标,并打印出来。

1
2
3
4
5
void Start()
{
GameObject cube = GameObject.Find("Lesson14");
print(Camera.main.WorldToScreenPoint(cube.transform.position));
}

第二题

2.在屏幕上点击一下鼠标,则在对应的世界坐标位置创建一个 Cube 出来。

1
2
3
4
5
6
7
8
9
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector3 pos = Input.mousePosition;
pos = Camera.main.ScreenToWorldPoint(new Vector3(pos.x, pos.y, 5));
GameObject.CreatePrimitive(PrimitiveType.Cube).transform.position = pos;
}
}

核心系统

核心系统—光源系统基础

光源组件 知识点

光源 - Unity 手册

知识点一 面板参数

知识点二 代码控制

UnityEngine.Light - Unity 脚本 API

大部分是成员,直接获取修改即可。

练习题

  1. 通过代码结合点光源模拟一个蜡烛的光源效果

    通过左右小幅度移动光源位置,小幅度调整光源强度实现。

  2. 通过代码结合方向光模拟白天黑夜的变化

    通过旋转方向光实现。

答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson15_Exercises : MonoBehaviour
{
public Light light;
public float moveSpeed = 10;
public float flashSpeed = 10;

public Transform lightTransform;
public float rotateSpeed = 10;

// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame
void Update()
{
#region 练习题一
//1.通过代码结合点光源
//模拟一个蜡烛的光源效果
//来回横跳
light.transform.Translate(Vector3.right * moveSpeed * Time.deltaTime);
if (light.transform.position.x >= 1)
moveSpeed = -moveSpeed;
else if (light.transform.position.x <= 0.8f)
moveSpeed = -moveSpeed;
//闪一闪
light.intensity += flashSpeed * Time.deltaTime;
if (light.intensity >= 1)
flashSpeed = -flashSpeed;
else if(light.intensity <= 0.5f)
flashSpeed = -flashSpeed;
#endregion

#region 练习题二
//2.通过代码结合方向光
//模拟白天黑夜的变化
lightTransform.Rotate(Vector3.right, rotateSpeed * Time.deltaTime);
#endregion
}
}

光面板相关 知识点

Lighting 窗口 - Unity 手册

核心系统—物理系统之碰撞检测

碰撞检测之刚体 知识点

面向对象的项目的 3D 物理系统 - Unity 手册

碰撞检测之碰撞器 知识点

知识点

注意

  1. 子物体的碰撞器参与父物体的刚体碰撞检测。
  2. 加了刚体的网格碰撞器必须启用 Convex,不然会报错
  3. 轮胎碰撞器是和整个车配合使用的,需要作为子物体,并且父物体拥有高质量和刚体,它会模拟车辆的物理效果。

碰撞检测之物理材质 知识点

1.创建物理材质

在 Project 窗口右键创建 Physic Material

2.材质属性

碰撞检测之碰撞检测函数 知识点

知识点回顾

  1. 如何让两个游戏物体之间产生碰撞(至少 1 个刚体 和 两个碰撞器)
  2. 如何让两个物体之间碰撞时表现出不同效果(物理材质)
  3. 触发器的作用是什么(让两个物体碰撞没有物理效果,只进行碰撞处理)

注意:碰撞和触发响应函数,属于特殊的生命周期函数,也是通过反射调用

知识点一 物理碰撞检测响应函数

UnityEngine.Collider - Unity 脚本 API

UnityEngine.Collision - Unity 脚本 API

关键参数

  1. 碰撞到的对象碰撞器的信息:collision.collider
  2. 碰撞对象的依附对象(GameObject):collision.gameObject
  3. 碰撞对象的依附对象的位置信息:collision.transform
  4. 触碰点数相关:collision.contactCount
  5. 接触点,具体的坐标:ContactPoint[] pos = collision.contacts;

只要得到了,碰撞到的对象的。任意一个信息,就可以得到它的所有信息

碰撞触发接触时,会自动执行这个函数

1
2
3
4
private void OnCollisionEnter(Collision collision)
{
print(this.name + "被" + collision.gameObject.name + "碰到了");
}

碰撞结束分离时,会自动执行的函数

1
2
3
4
private void OnCollisionExit(Collision collision)
{
print(this.name + "和" + collision.gameObject.name + "结束了贴贴~");
}

两个物体相互接触摩擦时,会不停的调用该函数

1
2
3
4
private void OnCollisionStay(Collision collision)
{
print(this.name + "和" + collision.gameObject.name + "一直在贴贴");
}

知识点二 触发器检测响应函数

触发开始的函数,当第一次接触时,会自动调用。

1
2
3
4
private void OnTriggerEnter(Collider other)
{
print(this.name + "和" + other.gameObject.name + "开始了贴贴");
}

触发结束的函数,当水乳相融的状态结束时,会调用一次。

1
2
3
4
private void OnTriggerExit(Collider other)
{
print(this.name + "和" + other.gameObject.name + "结束了贴贴");
}

当两个对象水乳相融的时候,会不停调用。

1
2
3
4
5
private void OnTriggerStay(Collider other)
{
print(this.name + "和" + other.gameObject.name + "在贴贴");

}

知识点三 要明确什么时候会响应函数

  1. 只要挂载的对象,能和别的物体产生碰撞或者触发,那么对应的这 6 个函数,就能够被响应。
  2. 6 个函数不是说,必须都得写;我们一般是根据需求来进行选择书写。
  3. 如果是一个异形物体,刚体在父对象上,如果你想通过子对象上挂脚本检测碰撞是不行的 必须挂载到这个刚体父对象上才行
  4. 要明确,物理碰撞和触发器响应的区别。

知识点四 碰撞和触发器函数都可以写成虚函数 在子类去重写逻辑

由于不需要我们自己调用这些碰撞函数,所以一般碰撞函数是私有或者保护的。

一般会把想要重写的,碰撞和触发函数,写成保护类型的;

没有必要写成 public,因为不会自己手动调用,都是 Unity 通过反射帮助我们自动调用的。

练习题

  1. 在之前 Input 和 Screen 中的练习题基础上,加入一个点击鼠标左键可以发射一颗子弹飞出的功能。
  2. 在上一题的基础上,加入子弹触碰到地面会自动消失的功能。
  3. 在上一题的基础上,在场景加入一些立方体,每个立方体被子弹打 3 下就会消失。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson11_Exercises : MonoBehaviour
{
public float moveSpeed = 10;
public float rotateSpeed = 10;

public Transform head;
public float headRotateSpeed = 50;

public Transform pkPos;
public float pkRotateSpeed = 20;

//子弹预设体
public GameObject bulletObj;
//子弹将要出现的位置
public Transform bulletPos;
void Update()
{
#region 练习题一
//1.使用之前的坦克预设体,用WASD键控制坦克的前景后退,左右转向
//Transform当中的 位移 自转 相关知识点
//键盘输入相关知识点

//Input.GetAxis("Horizontal"); 水平方向 -1到1之间的值 0就是没有按下
//Input.GetAxis("Vertical"); 竖直方向 -1到1之间的值 0就是没有按下
//ws键 控制位移
// 这公式 是 : 前进方向 * 速度 * 时间 * 当前是否移动(-1~1 相当于 正向还是反向的感觉 不按就不动 0)
this.transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime * Input.GetAxis("Vertical"));

//ad键 控制 左右转向
// 这公式 是 : 转动的轴 * 速度 * 时间 * 当前是否移动(-1~1 相当于 正向还是反向的感觉 不按就不动 0)
this.transform.Rotate(Vector3.up * rotateSpeed * Time.deltaTime * Input.GetAxis("Horizontal"));
#endregion

#region 练习题二
//2.在上一题的基础上,鼠标左右移动控制炮台的转向
//鼠标输入相关
//Input.GetAxis("Mouse X");
// 这公式 是 : 转动的轴 * 速度 * 时间 * 当前是否移动(-1~1 相当于 正向还是反向的感觉 不按就不动 0)
head.Rotate(Vector3.up * headRotateSpeed * Time.deltaTime * Input.GetAxis("Mouse X"));
#endregion

#region Lesson12 练习题一
//1.在输入习题的基础上,鼠标滚轮控制控制炮管的抬起放下
//Input.mouseScrollDelta.y
pkPos.Rotate(Vector3.right * pkRotateSpeed * Time.deltaTime * Input.mouseScrollDelta.y);
#endregion

#region Lesson16 练习题一
if( Input.GetMouseButtonDown(0) )
{
//实例化一个子弹对象
GameObject obj = Instantiate(bulletObj);
//设置对象的位置
obj.transform.position = bulletPos.position;
//角度
//obj.transform.rotation = bulletPos.rotation;
//角度
obj.transform.eulerAngles = bulletPos.eulerAngles;
}
#endregion
}
}

Bullet.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletObj : MonoBehaviour
{
public float moveSpeed = 10;
// Update is called once per frame
void Update()
{
this.transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
}

//private void OnCollisionEnter(Collision collision)
//{
// //问题一:如果发射子弹时 和坦克自身的碰撞和重合了 可能一开始 就会被移除
// //解决方案:判断自己碰撞到的对象 是什么 一定是特定对象 才移除自己
// if(collision.gameObject.CompareTag("Ground"))
// {
// //碰撞到别的东西 就让子弹小时
// //一定是移除自己依附的GameObject对象 而不是脚本自己
// Destroy(this.gameObject);
// }

// //问题二:坦克本身就带有碰撞盒 当子弹和坦克自身的碰撞盒碰撞可能会产生力的作用 出现一些意想不到的效果
// //解决方案:把子弹做成触发器 这样就没有了力的作用
//}

private void OnTriggerEnter(Collider other)
{
//问题一:如果发射子弹时 和坦克自身的碰撞和重合了 可能一开始 就会被移除
//解决方案:判断自己碰撞到的对象 是什么 一定是特定对象 才移除自己
if (other.gameObject.CompareTag("Ground") ||
other.gameObject.CompareTag("Monster"))
{
//碰撞到别的东西 就让子弹小时
//一定是移除自己依附的GameObject对象 而不是脚本自己
Destroy(this.gameObject);
}

//问题二:坦克本身就带有碰撞盒 当子弹和坦克自身的碰撞盒碰撞可能会产生力的作用 出现一些意想不到的效果
//解决方案:把子弹做成触发器 这样就没有了力的作用
}
}

CubeObj.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CubeObj : MonoBehaviour
{
public int HP = 3;

//当子弹碰到我时 就减血 血量为0了 就移除
private void OnTriggerEnter(Collider other)
{
//由于场景上 只有子弹时触发器 所以我们可以不用进行任何判断 就可以完成这个功能
//减血
HP -= 1;
//为0就移除自己
if( HP <= 0 )
{
Destroy(this.gameObject);
}
}
}

碰撞检测之刚体加力 知识点

UnityEngine.Rigidbody - Unity 脚本 API

知识点一 刚体自带添加力的方法

给刚体加力的目标就是:让其有一个速度,朝向某一个方向移动。

1.首先应该获取刚体组件

1
rigidBody = this.GetComponent<Rigidbody>();

2.添加力

加力过后,对象是否停止移动,是由阻力决定的。

如果阻力(Drag)为 0,那给了一个力过后,始终是不会停止运动。

相对世界坐标系,Z 轴正方向加了一个力

1
2
public void AddForce(Vector3 force)
rigidBody.AddForce(Vector3.forward * 10);

相对世界坐标系方法中,让对象相对于自己的面朝向移动。

1
rigidBody.AddForce(this.transform.forward * 10);

相对本地坐标施加力。

1
2
public void AddRelativeForce(Vector3 force)
rigidBody.AddRelativeForce(Vector3.forward * 10);

3.添加扭矩力,让其旋转

相对世界坐标

1
2
public void AddTorque(Vector3 torque)
rigidBody.AddTorque(Vector3.up * 10);

相对本地坐标

1
2
public void AddRelativeTorque(Vector3 torque)
rigidBody.AddRelativeTorque(Vector3.up * 10);

4.直接改变速度

如果要直接通过改变速度,来让其移动。这个速度方向,是相对于,世界坐标系的 。

一定要注意这一点。

1
2
rigidBody.velocity = Vector3.forward * 5;
rigidBody.angularVelocity = Vector3.forward * 5;

5.模拟爆炸效果

模拟爆炸的力,一定是所有希望产生爆炸效果影响的对象。

都需要得到他们的刚体,来执行这个方法,才能都有效果。

也就是说,这个爆炸只影响本身。

1
2
3
4
5
public void AddExplosionForce(float explosionForce, Vector3 explosionPosition, float explosionRadius);
//explosionForce:力大小
//explosionPosition:产生力的位置(世界坐标系)
//explosionRadius:爆炸力半径
rigidBody.AddExplosionForce(100, Vector3.zero, 10);

知识点二 力的几种模式

力的模式

所有添加力的方法都有一个重载,第二个参数为添加的力的模式。

第二个参数,力的模式主要的作用,是计算方式不同而已。

由于 4 中计算方式的不同,最终的移动速度就会不同。

默认模式为:Force

1
2
public void AddForce(Vector3 force, [DefaultValue("ForceMode.Force")] ForceMode mode)
rigidBody.AddForce(Vector3.forward * 10, ForceMode.Acceleration)
1
2
3
4
5
6
public enum ForceMode {
Force = 0,
Acceleration = 5,
Impulse = 1,
VelocityChange = 2
}

动量定理

F _ t = m _ v,力 _ 时间 = 质量 _ 速度
v = F * t / m;
F:力大小
t:时间
m:质量
v:速度

1.Acceleration

给物体增加一个持续的加速度,忽略其质量

v = F * t / m
F:(0,0,10)
t:0.02s(时间,物理帧间隔)
m:默认为 1(这里的 m 是质量 mass)
v = 10 * 0.02 / 1 = 0.2 m/s(这里的 m 是米)

每物理帧移动 0.2m/s * 0.02 = 0.004m(这里的 m 是米)

2.Force

给物体添加一个持续的力,与物体的质量有关

v = F * t / m
F:(0,0,10)
t:0.02s
m:2kg(这里的 m 是质量 mass)
v = 10 *0.02/ 2 = 0.1m/s(这里的 m 是米)
每物理帧移动 0.1m / s * 0.02 = 0.002m(这里的 m 是米)

3.Impulse

给物体添加一个瞬间的力,与物体的质量有关,忽略时间,默认为 1。

v = F * t / m
F:(0,0,10)
t:默认为 1
m:2kg(这里的 m 是质量 mass)
v = 10 *1 / 2 = 5 m/s(这里的 m 是米)

每物理帧移动 5m / s * 0.02 = 0.1m(这里的 m 是米)

4.VelocityChange

给物体添加一个瞬时速度,忽略质量,忽略时间

v = F * t / m
F:(0,0,10)
t:默认为 1
m:默认为 1(这里的 m 是质量 mass)
v = 10 * 1 / 1 = 10m /s(这里的 m 是米)

每物理帧移动 10m/s * 0.02 = 0.2m(这里的 m 是米)

比较符合正常逻辑的模式是 Force,几个参数都参与了计算。

知识点三 力场脚本

力场组件:Constant Force

添加此组件,会自动给未添加 Rigidbody 组件的物体添加 Rigidbody 组件。

可以直接修改 Constan Force 组件上的参数,来给物体施加一个持续的力。

练习题

请问现在让一个物体产生位移有几种方式?

  1. 修改 transform,使用 Translate 方法。
  2. 给拥有 Rigidbody 的物体施加力。
  3. 修改 Rigidbody 的 velocity,给物体添加速度。

答案

  1. 直接在 Update 生命周期函数中改变 Transform 当中的 Position 属性
  2. 直接在 Update 生命周期函数中使用 Transform 提供的 APITransLate 这个方法
  3. 通过加力
    1. rigidBody.AddForce
    2. rigidBody.AddRelativeForce
  4. 通过改变刚体速度变量:rigidBody.velocity = Vector3.forward * 10;

核心系统—音效系统

音频文件导入 知识点

音频文件 - Unity 手册

音频源和音频监听脚本 知识点

音频源 - Unity 手册

代码控制音频源 知识点

UnityEngine.AudioSource - Unity 脚本 API

UnityEngine.AudioClip - Unity 脚本 API

知识点一 代码控制播放停止

播放

1
audioSource.Play();

延迟(秒)播放

1
audioSource.PlayDelayed(5);

停止播放

1
audioSource.Stop();

暂停播放

1
audioSource.Pause();

停止暂停

1
audioSource.UnPause();

知识点二 如何检测音效播放完毕

如果你希望某一个音效播放完毕后,想要做什么事情。

那就可以在 Update 生命周期函数中,不停的去检测它的该属性。

如果是 false 就代表播放完毕了

1
audioSource.isPlaying

知识点三 如何动态控制音效播放

第一种方法

  1. 使用一个空物体挂载一个 AudioSource,做成 prefab。
  2. 直接在要播放音效的对象上挂载脚本,控制播放
  3. 需要播放时,实例化挂载了音效源脚本的对象,这种方法,其实用的比较少。
1
Instantiate(obj);

第二种方法

用一个 AudioSource 来控制播放不同的音效

1
2
3
AudioSource aus = this.gameObject.AddComponent<AudioSource>();
aus.clip = clip;
aus.Play();

潜在知识点
一个 GameObject 可以挂载多个 音效源脚本 AudioSource。使用时要注意,如果要挂载多个,那一定要自己管理他们,控制他们的播放、停止。不然,我们没有办法准确的获取谁是谁。

麦克风输入相关 知识点

UnityEngine.Microphone - Unity 脚本 API

知识点一 获取设备麦克风信息

1
2
3
4
5
string[] strs = Microphone.devices;
for (int i = 0; i < strs.Length; i++)
{
print(strs[i]);
}

知识点二 开始录制

参数一:设备名,传空使用默认设备
参数二:超过录制长度后,是否重头录制
参数三:录制时长
参数四:采样率

1
clip = Microphone.Start(null, false, 10, 44100);

知识点三 结束录制

参数为设备名,表示停止某个设备的录制。

1
Microphone.End(null);

知识点四 获取音频数据用于存储或者传输

UnityEngine.AudioClip - Unity 脚本 API

AudioClip-GetData - Unity 脚本 API

注意,对于压缩音频文件,仅当在音频导入器中将 Load Type 设置为 Decompress on Load 时才能检索样本数据。否则,将返回所有样本值均为零的数组。

规则:用于存储数组数据的 长度 = 声道数 * 剪辑长度。

1
2
3
float[] f = new float[clip.channels * clip.samples];
clip.GetData(f, 0);
print(f.Length);

Unity 入门总结

本期学习的主要内容

  1. Unity 环境搭建
  2. Unity 界面基础
  3. Unity 工作原理
  4. Unity 脚本基础
  5. GameObject
  6. Time
  7. Transform
  8. Input 和 Screen
  9. Camera
  10. 物理、光源、音效系统

学习 Unity 是学习什么?

  1. Unity 引擎核心系统的窗口操作和系统组件
  2. Unity 引擎公共 API
  3. 面向对象思想的应用

如何学好 Unity?

每学习一个 Unity 引擎的新知识点你脑海里面要思考,要举一反三。

要想想这个知识点,在游戏开发时可以用来完成什么功能。

所有的游戏功能,就是各个知识点有逻辑的排列组合在一起实现的。

强调

不要好高骛远

不要心浮气躁

不要急于求成

要积少成多,慢慢积累

理论结合实践的提升自己的编程和逻辑能力

必备知识点

必备知识点—场景切换和退出游戏

知识点一 场景切换

使用 SceneManager 提供的 API:LoadScene();

在加载场景前,需要把场景添加到场景列表当中

1
2
3
4
5
6
7
8
9
10
11
if( Input.GetKeyDown(KeyCode.Space) )
{
//切换到场景2
//直接 写代码 切换场景 可能会报错
//原因是没有把该场景加载到场景列表当中
SceneManager.LoadScene("Test2");

//用它不会报错 只会有警告 一样可以切换场景
//SceneManager
//Application.LoadLevel("Test2");
}

知识点二 退出游戏

退出只会在打包为 exe 文件之后才可以体现

1
2
3
4
5
6
7
if( Input.GetKeyDown(KeyCode.Escape) )
{
//执行这句代码 就会退出游戏
//但是 在编辑模式下没有作用
//一定是发布游戏过后 才有用
Application.Quit();
}

必备知识点—鼠标隐藏锁定相关

知识点一 隐藏鼠标

修改 Cursor 类中的 visible 属性

1
Cursor.visible = true;	//true显示,false隐藏

知识点二 锁定鼠标

锁定鼠标在游戏屏幕范围内。

None:不锁定
Locked::锁定鼠标会被限制在,屏幕的中心点;不仅会被锁定,还会被隐藏,可以通过 ESC 键,摆脱编辑模式下的锁定。
Confined:限制在窗口范围内。

1
Cursor.lockState = CursorLockMode.Confined;

知识点三 设置鼠标图片

UnityEngine.Texture2D - Unity 脚本 API

参数一:光标图片,Texture(纹理图片)

注意,光标的图片应该是正方形的,否则会发生扭曲;

并且如果光标的图片不是 png 格式,还需要设置材质 TextureType 为 Cursor

参数二:偏移位置,相对图片左上角

参数三:平台支持的光标模式(硬件或软件),一般用 Auto

1
Cursor.SetCursor(tex, Vector2.zero, CursorMode.Auto);

必备知识点—随机数和 Unity 自带委托相关

知识点一 随机数

Unity 中的随机数

Unity 当中 的 Random 类 此 Random(Unity)非彼 Random(C#)

使用随机数 int 重载,规则是左包含右不包含(左闭有开)

1
2
3
//0~99之间的数
int randomNum = Random.Range(0, 100);
print(randomNum);

float 重载,规则是左右都包含(左闭右闭)

1
float randomNumF = Random.Range(1.1f, 99.9f);

C#中的随机数

1
2
System.Random r = new System.Random();
r.Next(0, 100);

知识点二 委托

C#的自带委托,==Action==,==Func==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
System.Action ac = () =>
{
print("123");
};

System.Action<int, float> ac2 = (i, f) =>
{

};

System.Func<int> fun1 = () =>
{
return 1;
};

System.Func<int,string> fun2 = (i) =>
{
return "123";
};

Unity 的自带委托,==UnityEngine.Events.UnityAction==

注意:Unity 没有提供类似 Func 的委托

Events.UnityEvent - Unity 脚本 API

1
2
3
4
5
6
7
8
9
UnityAction uac = () =>
{

};

UnityAction<string> uac1 = (s) =>
{

};

必备知识点—模型资源的导入

主要内容

  1. 模型由什么构成
  2. Unity 支持的模型格式
  3. 如何指导美术同学导出模型
  4. 学习阶段在哪里获取模型资源

模型由什么构成

骨(骨骼)——非必须,有动作的模型才需要
肉(网格面片)——必须,决定了模型的轮廓
皮(贴图)——必须,决定了模型的颜色效果

Unity 支持的模型格式

官方推荐使用 FBX 格式的模型文件,其它格式虽然支持,但是不推荐。
支持的格式:.fbx,.dae,.3ds,.dxf,.obj

如何指导美术同学导出模型

Unity 官网,有指导手册,让美术同学查看手册按步骤导出即可。

https://docs.unity.cn/cn/2019.4/Manual/CreatingDCCAssets.html

重点规则:Unity 中模型面朝向朝模型坐标系的 Z 轴;缩放大小单位要注意

学习阶段哪里获取模型资源

  1. 官方 AssetStore(推荐)
  2. 淘宝(推荐)
  3. 一些第三方的资源下载网站

获取的资源一般两种形式

  1. Unity 资源包(更方便我们使用)
  2. 原生的模型贴图资源(需要我们自己进行一些设置)

总结

  1. 模型由什么构成——骨肉皮
  2. Unity 支持的模型格式——推荐 FBX
  3. 如何指导美术同学导出模型——官方文档
  4. 学习阶段在哪里获取模型资源——Asset Store 和淘宝

项目打包

BuildSetting

TargetPlatform:目标平台,win、mac、linux 等

Architecture:结构模式,x86、x64

ServerBuild:服务器构建,开启后可为应用程序编写特定于服务器的代码,方便调试

CopyPDBFile:开启后可以在打包出的数据中,包含程序数据库文件,之后可以用于调试应用程序

CreateVisualStudio Solution:开启后可以生成 VS 解决方案文件可以在 VS 中进行正式的打包可以设置更多额外信息

DevelopmentBuild:启用后,会在打包出的版本中开启性能分析器,之后可以用于调试

Autoconnect Profiler:启用 Development Build 后才有用,开启后 Unity 性能分析器可以自动连接到你的打包项目

Deep Profiling:启用 Development Build 后才有用,开启后,性能分析器可以分析到每个函数的调用来获取更详细数据

ScriptDebugging:启用 Development Build 后才有用,开启后,Unity 会将调试符号添加到脚本代码中

Scripts Only Build:启用 Development Build 后才有用,启用后可以提高打包时间,只会更新更改的脚本

Compression Method:压缩数据格式

  1. Default 不压缩
  2. LZ4:快速压缩格式
  3. LZ4HC:LZ4 的高度压缩变体,构建速度会变慢,但是压缩效果好

PlayerSetting-Player 部分内容

CompanyName:公司名

ProductName:产品名,游戏名

Version:版本号

Default Icon:默认图标

DefaultCursor:默认鼠标图标

CursorHostpot:鼠标图标偏移位置

Icon:自定义各种尺寸的游戏图标

Resolution

FullscreenMode

  1. FullScreenWindow:以游戏设定分辨率全屏
  2. ExlusiveFullscreen:适配全屏
  3. MaximizedWindow:最大化窗口化
  4. Window:窗口化

Default Is Native Resolution:勾选时,使用显示器分辨率;取消勾选时,使用设置的分辨率(窗口模式才有用)

Default Screen Height:游戏画面默认高度,窗口模式才有用

Mac Retina Support:是否在 Mac 系统上启用高 DPI 支持

Run In background:开启后,程序失去焦点时可以继续运行而不是暂停

实践总结

Unity 做游戏的套路

  1. C#相关知识(基础语法,面向对象等等)
  2. Unity 相关知识(API 的使用,系统脚本使用)

两者结合来进行游戏开发,你需要熟练掌握 C#知识,了解 Unity 提供的各功能可以帮助我们完成什么样的需求。

Unity 中:脚本+预设体+面向对象;脚本控制模型或者图片等预设体,让他们呈现出游戏逻辑比如移动、旋转、动态创建等等

如何学好 Unity

  1. 搞定前提条件——C#

    扎实的 C#基础可以让你事半功倍,特别是对面向对象的理解

  2. 养成惯性思考习惯

    在学习 Unity 的一个新知识点时,要在脑袋里思考这个知识点可以帮助我们制作怎样的游戏功能,产生映射记忆

C# + Unity + 面向对象+实践和思考 = 无所不能

达到目的

  1. 总结 Unity 做游戏的套路——C#+Unity+面向对象
  2. 结如何学好 Unity ——扎实的语言基础+映射记忆+实践+思考

练习

  1. 完善类图

    将实践小项目的类图重新绘制,理清关系,整理思路

  2. 拓展功能

    为实践小项目加一些自己的想法,比如:关卡选择,更多的坦克类型等等