Unity界面
左上角 - 层级(Hierarchy): 以树形式显示游戏内对象
左下角 - 项目(Project): 项目内导入的资源文件 - 控制台(Console): 用于游戏代码的打印输出调试
中间栏 - 场景(Scene): 以可视化形式展示游戏内容 - 游戏(Game): 对游戏进行预览
右侧 - 检查器/属性(Inspector): 显示资源和游戏对象的属性
场景
游戏中的场景是至关重要的, 它区别游戏中不同的活动空间, 给予玩家不同的视觉感受。它被储存在项目栏里的Scenes文件中。
场景的储存方式为。unity文件。
可见Unity中游戏开发文件的储存方式即为场景。
资源
资源(Asset)指游戏中使用到的素材, 包括:
图片素材Texture, 音频素材AudioClip, C#脚本素材C# Script等等。
资源的导入: 直接将需要的资源文件复制到Assets文件夹下
在资源文件夹中会自动生成导入的素材的。meta描述文件。
最好在Assets文件夹下创建一个Textures文件夹用来专门存放游戏贴图。
游戏贴图的裁剪:
对于一张图片中有多个素材的情况,Unity内置素材贴图切割
我们将素材的Sprite模式设置为多个,代表这个贴图中有多个素材
进入Sprite Editor,选择Slice可以进行自动切割。一个图片未经切割的话,便只包含一个Sprite。
游戏对象
我们在游戏中看到的一个个物件, 人物, 实体都被称作游戏对象, 是从游戏资源经过加工而实现在游戏内的模型, 贴图等。
如图, 将贴图拖动至层级栏内可以建立一个游戏对象
另外, 在游戏的场景文件中不会保存此场景内的游戏对象, 它保存的是游戏对象的引用和属性。
如图,这是检查器栏内游戏对象的属性,名字左边的对勾为Active选项,设置游戏组件的激活状态。
? 标签:用于对游戏对象进行分类,方便批量操作。
如图,这是检查器栏内的游戏对象精灵渲染器和额外设定。
若要修改2D项目内的贴图显示层级,要修改额外设定内的图层顺序(Order in Layer),大的在上。
或者直接修改Z轴坐标(不推荐)。
轴心(Pivot):
主要用来确定素材的中心位置,旋转原点。
如果轴心不在贴图的中心,贴图在旋转和移动的时候会有所不同。在选中一个贴图素材(注意不是游戏对象)后,检查器内点击Sprite Editor可以编辑贴图的轴心位置。
对象的父子关系
在层级栏中,我们可以指定游戏对象间的父子关系,以方便对象的批量操作,最简单的例子就是:子对象会随着父对象完全同步的移动和旋转。
配置:只需要将要设为子对象的游戏对象拖动为要设为父对象的游戏对象上面即可。
子对象相当于父对象的分支。
值得注意的是,子对象的坐标轴是相对于父对象的,在父对象移动和旋转时子对象不发生移动和旋转(牵连运动),子对象的坐标轴原点是父对象的轴心。
预制体
即预先制作好的游戏对象(模板)。将游戏对象预先制作好,作为资源备用。一般用于游戏对象的动态创建。
首先在Assets文件夹下新建用于存储大量预制体的文件夹Prefabs;将一个已经制作好的游戏对象拖入Prefabs文件夹,这里使用雪球。
这样就表明这个游戏对象是一个预制体实例,它有属于自己的预制体,使用预制体可以快速创建大量实例。预制体名字尽量改为***prefab的形式,避免混淆。
预制体的编辑:
预制体是一个游戏资源,双击可进入资源编辑模式
在游戏资源编辑器中,窗口左边的层级栏变成了该预制体的层级栏,只显示该预制体下的游戏对象和子对象。场景栏变成显示预制体的栏。
给雪球预制体挂载一个脚本:
//Update内
float step = 0.8f * Time.deltaTime;
transform.Translate(step, 0, 0, Space.Self);
每个用预制体生成的雪球实例都会自动挂载该脚本,也就是自动平移。
Prefab和PrefabInstance存在着联系,对预制体的修改可以被反映到全部预制体实例上,而对预制体实例的修改也可以保存到对应预制体上,最终反映到所有预制体实例当中。
当预制体实例被修改后,点击Overrides可以查看当前实例与预制体的不同之处。
可以选择重置该实例(RevertAll),也可以选择应用对该实例的修改到预制体上(ApplyAll)
中断一个实例与预制体的对应关系:
层级栏中右键点击要断开的实例,Prefab>Unpack执行断开操作,让这个实例成为独立的游戏对象。
动态创建实例:
使用滑雪人挂载的脚本,当按下鼠标左键时生成一个向前运动的雪球。
首先确保雪球预制体挂载的脚本令雪球会向右移动。在滑雪人脚本中:
public GameObject snowBallPrefab;
//Update内
if(Input.GetMouseButtonDown(0))
{
Instantiate(snowBallPrefab);
}
由于Instantiate函数重载的很多版本,这里列举几个常用的:
//在脚本挂载的游戏对象所在的position,rotation生成一个实例,并指定该实例的父级为根节点
GameObject snowBall = Instantiate(snowBallPrefab);
//在指定position,rotation生成一个实例,并指定该实例的父级为根节点
GameObject snowBall = Instantiate(snowBallPrefab, transform.position, transform.rotation);
//在脚本挂载的游戏对象所在的position,rotation生成一个实例,并指定该实例的父级为当前脚本挂载的游戏对象的transform.parent
GameObject snowBall = Instantiate(snowBallPrefab, transform.parent);
//在指定position,rotation生成一个实例,并指定该实例的父级为当前脚本挂载的游戏对象的transform.parent
GameObject snowBall = Instantiate(snowBallPrefab, transform.position, transform.rotation, transform.parent);
实例的销毁:
让雪球超出边界后销毁自己的函数:
Vector3 sp = Camera.main.WorldToScreenPoint(transform.position);
if (sp.x > Screen.width && sp.x < 0)
{
Destroy(gameObject);
}
坐标系
在选中一个游戏对象后, 检查器栏会调出被选中的游戏对象的各种属性, 其中Transform属性包括了该游戏对象的三个基本信息:
位置, 旋转角度, 缩放大小。
坐标单位: 在Unity的坐标系中, 约定一个方格为1x1单位(Unit), 在屏幕上是100像素, 在真实世界中可以自行约定。
Unity主要用于3D开发, 就算我们现在开发的是一个2D的游戏, 它仍然会给我们一个Z坐标。
X:横向右为正 Y: 纵向上为正 Z: 垂直于屏幕里为正
且在Unity中, 旋转是逆时针为正的。
皆由此我们的项目2D和3D转换成为可能。
3D视图导航器Gizmo界面
坐标与旋转:
Vector3变量:用来表示3维向量(x,y,z),也称为3元数。
//x,y,z为float类型
//在脚本运行时给予游戏对象一个新的坐标位置
transform.position = new Vector3(0,1.0f,0);
对于旋转角度,我们不使用transform.rotation,因为它是使用Vector4来表示旋转,比较复杂,我们使用:
//将角度转变成 欧拉角 来实现
//逆时针45度
transform.eulerAngles = new Vector3(0,0,45f);
如图,游戏对象位置和角度都发生了变化。
世界坐标,本地坐标:
WorldSpacePosition:以世界中心作为自己的坐标系。
LocalSpacePosition:以父节点轴心作为自己的坐标系。
例如:
新建雪球游戏对象并挂载同名脚本,其父级节点为滑雪人。
//在Start函数内
//将雪球初始位置设为父级节点右侧3个unit
transform.localPosition = new Vector3(3.0f, 0, 0);
运行游戏后观察到父级节点逆时针旋转45度后雪球依然被固定在父级节点右侧3个Unit处。
除此以外,LocalEulerAngle可以在父节点已经将子节点旋转过的情况下再次旋转子节点。
通过这个区别,可以简化很多复杂的位置算法。
向量
Vector2:二维向量的数据结构
Vector3:三维向量的数据结构
方向默认是原点到Vector坐标方向。
利用官方APi求向量终点到原点距离:
//先获取游戏对象的位置信息
//pos.magnitude获取位置到原点的距离
//打印输出二值
Vector3 pos = gameObject.transform.position;
float len = pos.magnitude;
Debug.Log(pos);
Debug.Log(len);
将一个向量变成单位向量的方法:
Vector3 pos = gameObject.transform.position;
pos = pos.normalized;
Debug.Log(pos.ToString("F3"));
//F3代表打印显示时保留3位小数
几种常用的标准向量:
Vector3.right//即Vector3(1,0,0)
Vector3.up//即Vector3(0,1,0)
Vector3.forward//即Vector3(0,0,1)
向量算数
将上述步骤在C#中实现:
//向量加法
Vector3 a = new Vector3(3, 1, 0);
Vector3 b = new Vector3(1, 2, 0);
Vector3 c = a + b;
Debug.Log("A: " + a.ToString("F3"));
Debug.Log("B: " + b.ToString("F3"));
Debug.Log("C: " + c.ToString("F3"));
//向量减法(d=b)
Vector3 d = c - a;
Debug.Log("D: " + c.ToString("F3"));
求两个点之间的距离(常用)
//获取两个不同游戏对象的位置
//输出两个向量间的距离
Vector3 pos1 = transform.Find("/Charater/Skiier").position;
Vector3 pos2 = transform.Find("/Charater/BigObject").position;
Vector3 distance = pos2 - pos1;
Debug.Log("Pos1: " + pos1);
Debug.Log("Pos2: " + pos2);
Debug.Log("Distance: " + distance.magnitude);
求两个点之间的夹角(常用)
//获取两个不同位置
//输出两个向量间的夹角
Vector3 pos1 = new Vector3(2, 2, 0);
Vector3 pos2 = new Vector3(-1, 3, 0);
float angle = Vector3.SignedAngle(pos1, pos2, Vector3.forward);
Debug.Log("夹角: " + angle);
值得注意的是,另外一个函数Angle(a,b)是求角度函数,不包括正负,即不关心时针方向。
练习:让一个游戏对象朝向另外一个
实现的原理是:两游戏对象的向量相减获得两对象直线向量,用这个向量和要旋转的对象的面朝方向向量求夹角,让要旋转的对象旋转这个夹角度数。
//获取目标朝向对象
GameObject target = transform.Find("/Charater/SnowBall").gameObject;
//获取目前朝向
Vector3 face = transform.right;
//计算出两对象相对向量
Vector3 direction = target.transform.position - this.transform.position;
//计算出目前朝向与相对向量夹角
float angle = Vector3.SignedAngle(face, direction, Vector3.forward);
//旋转该角度
transform.Rotate(0, 0, angle);
摄像机
场景界面当中的内容仅限于游戏开发中, 实际用户看到的画面则是摄像机拍摄出来的, 也就是游戏视图。
默认摄像机的位置为0,0,-10
大小(Size):代表摄像机的拍摄缩放大小,默认为5。
准备背景图片时:
Size=5 图片高度:(2x5)Unit x 100 = 1000px 图片宽度:跟随比例,如果摄像机比例是5:4,则图片宽度应为1000 x (5/4) = 1250px
Size=5.4 1920x1080
Size=4.5 1600x900
Size=3.6 1280x720
Size=2.7 960x540
屏幕坐标系
是以屏幕左下角为原点的直角坐标系,可以通过以下函数得到屏幕的宽高:
int screenW = Screen.width;
int screenH = Screen.height;
Debug.Log("Width:" + screenW);
Debug.Log("Height:" + screenH);
使用API来获取一个游戏对象的屏幕坐标系:
//获取游戏对象实际坐标
//使用API进行坐标转换
Vector3 pos = transform.position;
Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);
Debug.Log("screenPos: " + screenPos);
当改变屏幕大小的时候,这个值会变化。
练习:设计一个在屏幕上来回折返的滑雪人
实现的原理是:使用屏幕坐标判断滑雪人的坐标是否超出屏幕边界,如果超出,则折返。
private Vector3 screenPos;
private bool isRight = true;
private float step;
//Update内
step = 2.8f * Time.deltaTime;
transform.Translate(step, 0, 0);
screenPos = Camera.main.WorldToScreenPoint(transform.position);
if(isRight = true && screenPos.x > Screen.width)
{
isRight = false;
transform.eulerAngles = new Vector3(0, 0, -180);
}
if (isRight = false && screenPos.x < 0)
{
isRight = true;
transform.eulerAngles = new Vector3(0, 0, 0);
}
成功折返。
组件
组件(Components)用于实现某一种功能,例如Sprite Renderer组件用来指定游戏组件对应的贴图/音效/模型。
组件的创建:
首先我们需要在层级栏中新建一个空对象,在它的检查器中选择添加组件,为它添加一个SpriteRenderer,将素材贴图拖入。
Transform组件:
变形(Transform)组件是游戏对象最基本的组件,代表了游戏对象的位置信息,即便是刚创建的空对象也会默认添加Transform组件(不可移除)。
游戏脚本
Unity中的游戏脚本语言为C#(Csharp Script)编译器:VisualStudio2019。
我们要给游戏对象添加脚本,首先在Asset文件夹下建立Scripts脚本文件夹,用来存放游戏对象使用的所有脚本。
在里面新建脚本(C# Script)。
脚本的使用:
1,新建脚本命名为Hello.cs;
2,将脚本挂载到游戏对象上;
3,运行游戏,在Console上观察脚本输出;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Hello : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
Start函数在开始的时候被调用一次,之后不会再被调用。
我们在Start函数中写下:
Debug.Log("Hello World!");
保存后回到Unity,选中游戏对象,添加组件,脚本,Hello.cs,这样就可以完成脚本的挂载。
然后点击场景视图上方的Play按钮,运行游戏
Hello World!完成输出。
另外,要先停止游戏再进行修改。
脚本的组件:
VisualStudio内置的解决方案资源管理器:
类Hello必须与脚本名字完全相同,并且所有Unity脚本必须继承于MonoBehaviour。
Start函数和Update函数会被Unity自动调用。
Start:脚本开始时的第一帧调用。
Update:脚本开始后的每一帧调用。帧间隔与操作系统有关。且帧间隔是不固定的。
内部顺序:
1,游戏对象被创建;
2,脚本创建组件 Hello comp = new Hello();
3,comp.Start();
4,定时执行comp.Update();
再Start函数中:
Application.targetFrameRate = 50;
用于显式设定游戏帧率。
对象的移动
在Update函数中,我们可以让游戏对象随着时间进行移动,具体代码:
this.transform.Translate(0, 0.05f,0);
//this.transform代表当前游戏对象的组件transform
//Translate(dx,dy,dz)相对位移
然而因为每一帧间隔是不一样的,速度=路程/时间,时间大小不断改变,而路程不变。所以虽然我们看起来它是匀速的,但还是有些许误差。
解决方式:
float step = 0.8f * Time.deltaTime;
this.transform.Translate(0, step,0);
这样的方式,让每帧移动的距离随帧间隔的变化等比缩放,可以达到真正的匀速。
移动方向的一些拓展:
当我们尝试给游戏对象设置一个旋转角度,就能看到游戏对象朝向你设置的旋转角度匀速移动。
这是因为Translat函数中为你缺省了一个参数:
//Space.Self代表此移动基于游戏对象自己的坐标系
//Space.World代表此移动基于游戏世界的坐标系
float step = 0.8f * Time.deltaTime;
this.transform.Translate(0, step,0,Space.Self);
当我们将初始游戏对象旋转角度设置成180度的时候,可以发现游戏对象是向下移动的,这是因为角度的旋转将游戏对象的坐标系倒过来了。
获取游戏的节点和组件
当前节点:this.gameObject
当前组件:this
获取渲染器组件,并尝试设置渲染器中的Y轴翻转为true:
SpriteRenderer renderer = this.gameObject.GetComponent<SpriteRenderer>();
//或
SpriteRenderer renderer = this.GetComponent<SpriteRenderer>();
//获取游戏的渲染器renderer
//GetComponent<组件类型>
renderer.flipY = true;
获取游戏中其他节点:
挂载到一个游戏对象的脚本内可以获取其他游戏对象节点,也能获取其他游戏对象的组件。
例如:
//通过新建GameObject获取指定路径下的游戏对象
GameObject obj = GameObject.Find("/Charater/BigObject");
SpriteRenderer renderer = obj.GetComponent<SpriteRenderer>();
renderer.flipY = true;
成功!
节点的父子关系
通过查阅Unity官方文档,可以发现,Transform组件还有维持游戏节点父子关系的功能。所以,Transform.parent可以找到一个节点的父级节点,也可以使用foreach函数遍历一个节点的所有子节点。
通过SetParent实现节点的子化。
获取父节点:
//获取当前脚本挂载到的节点的父节点
GameObject parent = this.transform.parent.gameObject;
Debug.Log(parent.name);
输出了父节点的名称。
遍历所有子节点(不做测试):
//child的类型:Transform(一个组件)
foreach(Transform child in transform)
{
Debug.Log(child.name);
}
变换父节点操作(不做测试):
//实现了父子节点的嫁接
GameObject obj1 = GameObject.Find("子节点");
GameObject obj2 = GameObject.Find("目标父节点");
obj1.transform.SetParent(obj2.transform);
//或者把父节点设置为null,代表挂载到场景根节点下(无父节点)
obj1.transform.SetParent(null);
调试
点击行数左边的灰白栏新建断点,调试模式开始后代码会在此处暂停。
点击上方的附加到Unity进入代码调试模式,
按下 逐过程(F10) 可以执行下一句。
事件函数
Unity内置有不同时刻执行不同函数的功能,常见的事件函数有:
Awake():脚本组件实例化时调用
OnEnable():脚本组件启用时调用
Start():脚本组件第一次执行前调用
Update():每一帧调用
FixedUpdate():Update的一个修正
都属于MonoBehaviour的方法。
事件函数分为两种,不以On打头的(Awake,Start…):由系统主动调用;以On打头的(OnEnable,OnDisable,OnGUI…):相应事件的回调函数
红字仅为个人理解。
实验:观察几个函数的调用顺序:
public class XXX : MonoBehaviour
{
private void Awake()
{
Debug.Log("This is Awake()");
}
private void Start()
{
Debug.Log("This is Start()");
}
private void OnEnable()
{
Debug.Log("This is OnEnable()");
}
private void Update()
{
Debug.Log("This is Update()");
}
private void FixedUpdate()
{
Debug.Log("This is FixedUpdate()");
}
private void OnApplicationQuit()
{
Debug.Log("This is OnApplicationQuit()");
}
}
值得注意的是,就算脚本已经被禁用,它在游戏开始的时候还是会执行Awake实例化函数,除此以外不会执行。当游戏运行时启用脚本会执行Start函数和OnEnable函数,但如果禁用脚本再打开,它便只会执行OnEnable函数。
所以:
Awake实例化函数只在游戏开始后脚本实例化时调用,与脚本是否被禁用无关。
Start函数只在脚本第一次执行时被调用一次,之后无论脚本禁用还是启用都不执行。
OnEnable函数会在每次脚本被启用的时候调用。
脚本的执行顺序
先遍历所有脚本的Awake()函数,然后遍历所有脚本的Start()函数,每一帧遍历所有脚本的Update()函数。哪个脚本先执行,哪个脚本后是优先级(ExecutionOrder)调控的,这个顺序我们可以在项目设置(ProjectSetting)里调整。
默认时间就是优先级为0,优先级值越小,越先执行。
若想将一个脚本优先于其他脚本执行,则将执行顺序设置为-1;
若想将一个脚本最后执行,则将执行顺序设置为1;
没有特别要求不要设置顺序。
脚本的参数
在脚本中定义的public类型变量,会被显示在检查器界面的脚本下,可直接修改参数来赋值。
给滑雪人挂载的脚本添加一个参数不给它赋值然后使用:
public Vector3 speed;
//Update内
Vector3 step = speed * Time.deltaTime;
transform.Translate(step,Space.Self);
脚本内出现这样的参数,给它一个值然后运行,可以看到游戏对象开始运动。
我们使用的Vector3,Vector2数据结构都是struct类型(值类型 Value Type),它的一个特点就是不能初始化为null,它总是有值的。
一个游戏脚本可以被多个游戏对象挂载。
引用类型的参数:
在脚本中,可以引用游戏对象,组件,资源…
//通过三种不同的引用,达到修改其他游戏对象贴图的效果
//引用资源作为参数,并赋值
public Sprite newSprite
//Start内
//引用游戏对象SnowBall
//游戏对象也可以使用参数的形式引用
GameObject SnowBall = transform.Find("/Charater/SnowBall").gameObject;
//引用游戏对象的组件
SpriteRenderer renderer = SnowBall.GetComponent<SpriteRenderer>();
//改变组件的参数
renderer.sprite = newSprite;
运行后:雪球改变了贴图。
此外,我们可以使用sprite.rect获取贴图的大小(矩形),sprite.rect.width获取贴图的宽度。
可以通过transform.LocalScale来改变游戏对象的缩放大小(参数为一个Vector3型的三位缩放比例)
延迟调用API:
Invok函数可以让它指向的函数在被触发一定延时后被调用,且这个过程没有涉及到多线程,原理是每个Update函数执行周期过后会检测有没有要执行的Invoke操作,要注意Invoke的参数是要调用的函数的名字(字符串),使用了反射机制。
多次快速延时调用同一个Invoke不会产生覆盖问题。
private float TimeVal;
void Update()
{
TimeVal += Time.deltaTime;
if(Input.GetMouseButtonDown(0))
{
Debug.Log("PING" + TimeVal);
Invoke("Pong", 3f);
}
}
void Pong()
{
Debug.Log("PONG" + TimeVal);
}
附加一些常用的API函数:
1,生成int/float随机数 value=Random.Range(min,max)
2,取消延时调用 CancelInvoke(method)
2,检测一个延时调用是否在等待中 IsInvoking(method) (返回一个布尔型值)
3,延时循环调用 InvokeRepeating(method,delay,interval)
消息调用
多个脚本之间有时候需要互通数据,实现的方式很多,最简单的方式是将脚本当作游戏对象的组件来使用,接收者可以获取数据提供者的public类型的变量和函数。在层级中新建两个游戏对象并分别挂载脚本,一个作为数据提供者(Producer),一个作为数据接收者(Consumer),设计一次两脚本间数据的交互:
数据提供者:
public float data = 1.234f;
数据接收者:
void Start()
{
GameObject producer = transform.Find("/Producer").gameObject;
Producer pro = producer.GetComponent<Producer>();
Debug.Log("Data: " + pro.data);
}
使用消息调用来执行对方脚本的函数:
数据提供者:
private float data = 1.234f;
void ShowData()
{
Debug.Log("Data: " + data);
}
数据接收者:
void Start()
{
GameObject producer = transform.Find("/Producer").gameObject;
producer.SendMessage("ShowData");
}
这个过程被称为消息调用,接收者向提供者发送了输出它的数据的命令。
**SendMessage方法(寻找方法并立即执行)**的参数是对方脚本的方法的名字(字符串)和那个方法需要的参数。
鼠标/键盘操作
Unity的InputAPI可以读取鼠标位置,读取到的鼠标位置为一个屏幕坐标。
设置雪球跟随鼠标位置移动:
实现的思想:获取鼠标屏幕坐标并转化成世界坐标,将这个坐标的位置赋值给雪球。
//雪球的脚本内
private Vector2 mousePos;
//Update内
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
transform.position = mousePos;
现在雪球就跟着鼠标移动了。
关于为什么鼠标位置的坐标不使用Vector3:
我们从InputAPI获得的鼠标位置在摄像机看来Z轴上值是0,所以当我们使用ScreenToWorldPoint函数转换鼠标坐标的时候,鼠标位置的世界坐标在屏幕坐标Z轴为0的情况下被转换成了-10,这会导致贴图被背景覆盖而不显示。
另一个解决方法:只给雪球的x,y赋值
transform.position = new Vector3(mousePos.x, mousePos.y, 0);
//或者
mousePos.z = 0;
练习:让滑雪人跟随鼠标旋转
实现的思想:获取鼠标位置,将滑雪人所在位置与鼠标位置相减得到滑雪人到鼠标位置的向量,使用Vector3的夹角函数获得两物体的夹角,将滑雪人旋转该角度。
//滑雪人脚本内
private Vector3 mousePos;
private Vector3 direction;
private float angle;
//Update内
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
mousePos.z = 0;
direction = (mousePos - transform.position).normalized;
angle = Vector3.SignedAngle(transform.right, direction, transform.forward);
transform.Rotate(0, 0, angle);
雪球的位置即为鼠标位置,滑雪人也面朝鼠标位置,鼠标移动也是如此。
鼠标按下:
使用Input API的简单判断左键是否按下函数:
if(Input.GetMouseButtonDown(0))
{
Debug.Log("MouseDown!");
}
GetMouseButtonDown函数的参数:
0,鼠标左键
1,鼠标右键
2,鼠标中键(按下)
3,鼠标侧键(下)
4,鼠标侧键(上)
练习:当鼠标按下的时候滑雪人朝鼠标位置移动
实现的思想:当我们按下鼠标左键的时候滑雪人面朝鼠标位置并匀速向前运动。
private float step;
private Vector3 mousePos;
private float angle;
private Vector3 direction;
//Update内
if(Input.GetMouseButton(0))
{
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
mousePos.z = 0;
direction = (mousePos - transform.position).normalized;
angle = Vector3.SignedAngle(transform.right, direction, transform.forward);
transform.Rotate(0, 0, angle);
transform.Translate(step, 0, 0);
}
设置滑雪人随着键盘输入的上下左右移动
Input API的GetKey函数可以帮助我们实现这个功能,具体代码:
//Update内
//按下哪个方向键便产生哪个方向的移动
float step = 1.8f * Time.deltaTime;
if(Input.GetKey(KeyCode.LeftArrow))
{
transform.Translate(-step, 0, 0);
}
if (Input.GetKey(KeyCode.RightArrow))
{
transform.Translate(step, 0, 0);
}
if (Input.GetKey(KeyCode.UpArrow))
{
transform.Translate(0, step, 0);
}
if (Input.GetKey(KeyCode.DownArrow))
{
transform.Translate(0, -step, 0);
}
物理系统
牛顿第一定律:F=m(v/t)=0
牛顿第二定律:F=ma
牛顿第三定律:F(1->2) = -F(2->1)
刚体(RIgidBody)
在之前,我们任何一个物体的移动都只是不停给物体的transform.position赋新值而达成的,从现在开始我们学习如何给予一个物体真正的速度。刚体是个组件,它规定一个游戏对象可以拥有自己的质量、速度、弹性、摩擦力……
在2D项目中,我们给物体添加的刚体组件为:RigidBody2D
添加了刚体的对象,因为有了质量和重力大小,在游戏开始时便会自动下坠
刚体的三种类型:
动态(Dynamic):代表普通刚体,有质量,有速度。
运动学刚体(Kinematic):无质量刚体,一般用于碰撞检测。
静态刚体(Static):质量无穷大,无速度,适用于建筑,地面等固定物体。
刚体的碰撞
根据上述知识,我们在场景中放置一个平面(静态刚体),一个红球(普通刚体),一个绿球(普通刚体)。
开始游戏后:两个球直接穿过了地面,没有发生碰撞。
这是因为我们的三个刚体虽然有了质量和速度,但是没有体积,或者说是用于与其他物体发生碰撞的边,对此我们需要给三个刚体加上碰撞体(Collider)这一属性。
给地面加上盒状碰撞体(BoxCollider2D),给两个球加上球状碰撞体(CircleCollider2D)
球下落->检测到碰撞->根据两刚体的质量,速度,弹性系数,摩擦力等计算->地面静止,球速度减为0
刚体的反弹:
目前的红球与绿球在与地面发生碰撞后速度直接归零,我们希望让它们可以反弹
首先在Assets目录下建立Materials文件夹,用来存储物体的材质文件。
新建2D->物理材质2D,取名为Basketball,弹力(Bounciness)设置为1,摩擦系数(Friction)不用管
将绿球刚体组件的材质属性赋值为这个材质。
现在我们的绿球会进行无能量损耗的反弹。
运动学刚体的碰撞检测
运动学刚体(Kinematic)代表质量为0的刚体。当我们将游戏对象的刚体类型设置为运动学刚体后,它们将不会因重力发生下坠。
给两个球加入球状碰撞器,勾选isTrigger,表明这个碰撞器在于其他碰撞器相遇的时候会发生事件。
给红球绿球挂载脚本:
红球:
//Update内
//让红球向右运动
float step = 0.8f * Time.deltaTime;
transform.Translate(step, 0, 0,Space.Self);
//建立回调函数
//OnTriggerEnter2D代表两物体碰撞箱重叠的第一帧调用的函数
private void OnTriggerEnter2D(Collider2D collision)
{
Debug.Log("RedBall: Activated");
}
绿球:
//建立回调函数
private void OnTriggerEnter2D(Collider2D collision)
{
Debug.Log("GreenBall: Activated");
}
类似回调函数:
OnTriggerStay:只要某一帧内两碰撞器有重叠的地方,那么这一帧就调用该函数。
OnTriggerExit:两碰撞器不再有重叠部分的第一帧调用一次该函数。
OnTriggerEnter2D(Collider2D collision)函数的参数collision代表的是与该函数挂载的游戏对象相碰撞的游戏对象,在脚本看来是“对方”。
collision.gameObject 对方
collision.transform 对方的Transform组件
collision.name 对方的名字
collision.tag 对方的标签类型
//红球脚本内
private void OnTriggerEnter2D(Collider2D collision)
{
Debug.Log("collision's name:" + collision.name);
}
销毁对方的操作:
//红球脚本内
private void OnTriggerEnter2D(Collider2D collision)
{
Destroy(collision.gameObject);
}
使用Tag来分类不同游戏对象
标签(Tag)的作用是对多个类似游戏对象进行归类,批处理等操作,选中一个Tag相当于选中Tag值为此的所有游戏对象,相当于身份标识。
碰撞回调中,多使用collision.tag来判断对方的类型。
我们使用预制体新建一些绿球和蓝球,并分为两个Tag:TeamGreen和TeamBlue
在Editor选项的ProjectSetting里可以调整标签和图层。
重写红球的回调函数并观察返回值:
private void OnTriggerEnter2D(Collider2D collision)
{
Debug.Log(collision.tag);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2NOYm6RZ-1636700555875)(E:\Paintings\UnityProject\test\PIC\image-20211110214200971.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dQwt7eer-1636700555875)(E:\Paintings\UnityProject\test\PIC\image-20211110214229918.png)]
身份识别的重要性:根据tag区分碰到不同对象的时候触发的函数。
层与碰撞矩阵
层(Layer)也可以用于归类区分不同的游戏对象,但它不可以像tag一样作为碰撞发生后检测的参数在脚本间被传递,它与碰撞矩阵(LayerCollisionMartix)配合,规定哪些层与哪些层不会发生碰撞,哪些会发生碰撞。在ProjectSetting的标签和图层中可以找到Layer选项,用它来新建层,我们给三种球设置LayerRed,LayerGreen,LayerBlue。在ProjectSetting的Physics2D选项中可以找到碰撞矩阵,规定哪些层与哪些层可以/不可以碰撞:
这样红球只能与绿球发生碰撞。
声音系统
Unity中声音的两种类型:2D声音/3D声音
2D声音:普通的向用户音频输出设备输出声音
3D声音:根据玩家在游戏当中的实际位置与音源位置而调整声音的播放大小,方向,左右声道的强度。
Assets中新建Audios文件夹用于存放使用的音频(AudioClip),然后将游戏制作需要的音频拖放进去。检查器窗口中可以查看音频的各种参数。
在Unity中,进行声音的播放需要两个游戏对象,一个作为声音源(AudioSource),一个作为声音接收者(AudioListener)。两个东西被作为组件添加到Unity中。mainCamera默认挂载了一个AudioListener组件,游戏空间中所有声音源都会被接收,并根据距离来合成。
我们创建一个游戏对象作为声音源,勾选唤醒时播放(PlayOnAwake),将一个音频文件拖入AudioClip属性下。
在代码中播放音频
//有关音频的一些API
//参数:
clip/mute/loop/volume/isPlaying
//方法:
Play();//停止当前正在播放的音频,重新开始播放
Stop();
Pause();
PlayOneShot(clip)//另开始播放一个clip音频
//Update内
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
mousePos.z = 0;
float distance = (mousePos - transform.position).magnitude;
if (distance < 1)
{
//当鼠标点击音源的时候播放音频
//如果音频在播放则无效
if(Input.GetMouseButtonDown(0))
{
AudioSource audio = GetComponent<AudioSource>();
if (!audio.isPlaying)
{
audio.Play();
}
}
//如果右键点击则停止播放
if (Input.GetMouseButtonDown(1))
{
AudioSource audio = GetComponent<AudioSource>();
if (audio.isPlaying)
{
audio.Stop();
}
}
}
如果我们播放的不是一个音乐而是一个短暂的音效,尽量使用PlayOneShot(clip),以防止重复播放让前一个播放还未完毕就被中断
//Update内
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
mousePos.z = 0;
float distance = (mousePos - transform.position).magnitude;
if (distance < 1)
{
if(Input.GetMouseButtonDown(0))
{
AudioSource audio = GetComponent<AudioSource>();
audio.PlayOneShot(audio.clip);
}
}
快速点击声音源可以让它反复播放而不打断任何一段音频
交互界面UI
UI不属于游戏空间,而是浮在游戏界面上。
对于Unity,所有游戏UI都被包含在Canvas(画布)层,在层级栏中新建UI->Canvas,与此同时一个EventSystem也被创建,它用于UI与游戏内的事件响应。观察Canvas的参数,调整渲染模式(RendererMode)为 屏幕空间-摄像机,然后把主摄像机的游戏对象拖进去,UI就可以被固定在当前主摄像机上,平面距离(PlaneDistance)代表Canvas与摄像机的Z轴向距离。
在层级栏中新建的UI对象会被自动添加成为Canvas的子对象,新建一个文本框对象:
文本框的一些重要属性:
文本(Text):规定文本框显示什么,经常用脚本来给它赋值。
字体(Font):文字的字体形态可以使用.ttf/.otf形式的字体文件。
对齐方式(Alignment):规定文字在文本框中的对齐位置。
最佳适应(BestFit):让文字自动根据文本框的长宽设定大小。
所有UI对象都有一个RectTransform(矩形变换组件),类似于游戏对象的Transform组件,它的单位是像素,不是Unit
Image对象
UI中有Image对象可以使用,在UI界面中展示希望展示的图片
新建一个Image对象并将贴图赋值给它
图中表示了Image的四个模式:简单(左上),切片(左下),平铺(右下),填充(右上)
贴图的SpriteEditor:
Button对象
观察层级栏和检查器,其实Button是一个集成了Image对象,Text对象和自身属性的组合对象。可以设置鼠标点击事件对它的各种影响,包括图片切换,变色,淡化过渡。
鼠标单击(OnClick)事件:可以让用户点击按钮的时候触发的函数,只需要在里面添加需要触发的脚本内的某个函数即可
新建一个游戏对象GameCtrl并挂载同名脚本,在里面加入一个公共无参点击事件函数:
public void Clicked()
{
Debug.Log("Click!");
}
在按钮点击事件中添加需要触发此函数的游戏对象->被挂载的脚本->函数。
练习:点击按钮时返回一个输入框内的文本
新建输入文本框(InputField),在GameCtrl脚本中:
using UnityEngine.UI;
//将输入文本框游戏对象拖入
public InputField textField;
public void Clicked()
{
string text = textField.text;
Debug.Log("Clicked,text: " + text);
}
不使用已有组件的事件处理
当我们想给现在的按钮加一些复杂的特效,或者想让一个单纯的文本和图片变成按钮,我们就需要使用Unity官方给的接口来实现。
/*
Pointer鼠标指针点击接口
IPointerEnterHandler - OnPointerEnter - 当鼠标移到 A 对象上的时候触发一次
IPointerExitHandler - OnPointerExit - 当鼠标移开 A 对象的时候触发一次
IPointerDownHandler - OnPointerDown - 当鼠标按下 A 对象的时候触发一次
IPointerUpHandler - OnPointerUp - 当鼠标松开 A 对象的时候触发一次
IPointerClickHandler - OnPointerClick - 当鼠标一次性点击并松开 A 对象时候触发一次
Drag拖拽接口
IInitializePotentialDragHandler - OnInitializePotentialDrag - 当鼠标按下还没开始拖拽 A 对象时触发一次
IBeginDragHandler - OnBeginDrag - 当鼠标按下并开始拖拽 A 对象时触发一次
IDragHandler - OnDrag - 当鼠标按下并拖拽 A 对象的每一帧触发一次
IEndDragHandler - OnEndDrag - 当鼠标停止拖拽并松开 A 对象时触发一次
IDropHandler - OnDrop - 当鼠标从 A 对象上开始拖拽,在 B对象上松开时 B 对象触发一次
滚动滚条
IScrollHandler - OnScroll - 当鼠标滚轮滚动时触发一次
选择组事件
必须设置选择对象后才能触发EventSystem.current.SetSelectedGameObject(gameObject)
IUpdateselectedHandler - OnUpdateSelected - 选中 A 对象的每一帧触发一次
ISelectHandler - OnSelect - 选中 A 对象触发一次
IDeselectHandler - OnDeselect - 取消选择 A 对象触发一次
InputManager关联组事件
必须设置关联对象后才能触发
IMoveHandler - OnMove - 移动(Horizontal和Vertical按键)
ISubmitHandler - OnSubmit - Enter键
ICancelHandler - OnCancel - Esc键
*/
练习:实现按钮按下时的缩放
调用接口应当将接口名放置在类的定义行中,使用IPointerDownHandler接口函数和IPointerUpHandler接口函数实现事件处理
//指定命名空间
using UnityEngine.EventSystems;
public class GameCtrl : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
//调用接口函数
public void OnPointerDown(PointerEventData eventData)
{
//变大
transform.localScale = new Vector3(1.1f, 1.1f, 0);
}
public void OnPointerUp(PointerEventData eventData)
{
//变小
transform.localScale = new Vector3(1, 1, 0);
}
}
射线投射(RayCast)机制:鼠标点击UI元素实现功能的内部机制被称为射线投射机制,Canvas层自带RayCaster负责发射射线探测,而接收射线的组件,也就是我们要点击的UI元素是RayCastTarget,负责接收射线探测。
布局
UI中用于规定对象位置的组件RectTransform本质上是Transform的一个子类,但是单位是像素而不是unit,可以定义组件的位置,宽高。
锚点(Anchor):这个值设定了当前组件的范围。默认为Canvas的中心,主要用于对齐UI的显示,以防屏幕界面的变化导致UI的异常显示:
缩小前:
缩小后:
可以直接进行粗略的锚点设置以及拉伸,被拉伸的组件会根据屏幕大小自动调整宽高,也可以直接使用锚点属性进行精确设置。总体上来说,锚点规定了一个UI组件的最大宽高,通过不断调整组件在锚点范围内的位置和宽高(调整依据参考轴心),来保证它不会超出这个范围。
UI子对象-Panel
面板(Panel)的作用是作为Canvas的子对象,在一个Canvas中定义一个”小Canvas“。规定这个面板中的子对象的锚点都是基于这个面板的范围。
练习:点击红球让得分加一并显示在UI上
红球代码:
void Update()
{
//如果点击红球则向gameCtrl发送触发函数命令
if (Input.GetMouseButtonDown(0))
{
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
mousePos.z = 0;
float distance = (mousePos - transform.position).magnitude;
if (distance < 1)
{
GameObject gameCtrl = transform.Find("/GameCtrl").gameObject;
gameCtrl.SendMessage("AddScore");
}
}
}
GameCtrl代码:
using UnityEngine.UI;
//引用分数显示UI组件
public Text scoreText;
public int score = 0;
//加分函数
public void AddScore()
{
score += 1;
scoreText.text = score.ToString();
}
|