Unity——UGUI开发设计及案例讲解
1. Unity4.6跟以前的版本的最大区别首先在于在层级视图中点鼠标右键时出现的弹出菜单上,它把以前许多的菜单项进行了归类,比如cube sphere capsule….等整合到“3D Object”子菜单中,而且多出一个UI子菜单,它就是UI组件了。
    当我们点击UI->Canvas时,就会在场景视图里创建一个画布,不过视图默认是3D显示方式,在Unity4.3以后,增加了一个2D与3D转换按钮,单击此按钮可在3D与2D显示样式之间转换。 
    UI是2D,为何还要3D呢,在做一些比较复杂的项目时,给用户看时是2D,但我们编辑时,有时还是要用到3D模式的。
2. Canvas是画布,所有的UI组件就是绘制在这个画布里的,脱离画布,UI组件就不能用。
    创建画布有两方式。一是通过菜单直接创建,二是直接创建一个UI组件时,会在创建这个组件的同时自动创建一个容纳该组件的画布出来。
    不管那种方式创建出画布时,系统都会自动创建出一个EventSystem组件,这是UI的事件系统。
一.Canvas组件
1 Canvas组件的三种渲染模式
在Canvas中有一Render Mode属性,它有3个选项,分别对应Canvas的三种渲染模式:Screen Space – Overlay、Screen Space – Camera、World Space

2 Screen Space – Overlay:

此模式不需要UI摄像机,UI将永远出现在所有摄像机的最前面(即在某个UI的前面是不能再添加其他组件的),就好像是给摄像机贴上了一层膜。它的最大好处是不需要摄像机,不需要灯光。

3 Screen Space – Camera:

此模式需要提供一个UICamera,它支持在UI前方显示3D模型与粒子系统等内容。
    不过此模式下,就需在 中给它挂一个摄像机。

当挂上摄像机并选择3D显示模式时,我们选中这个摄像机,并移动它,可发现画布会跟随摄像机的移动而移动,且Game视图显示的UI其位置与大小均保持不变,如下图所示:

这种模式,虽然UI的显示效果与第一种模式没有什么两样,然而,因在画布与摄像机之间可放置三维物体或粒子系统,那么就可做出许多绚丽的特效。
      这一项是设置Canvas与摄像机之间的距离,其值越大,可在画布与摄像机之间放很多的三维物体,默认是100,建议设置为100与200之间即可。
4 World Space:
 
这个就是完全3D的UI,也就是把UI也当成3D对象,如摄像机离UI远了,其显示就会变小,近了就会变大。
5 其它一些属性:
 当有多个画布时,决定谁在前,谁先显示。
二.Canvas Scaler画布的大小
 
Ui Scale Mode(大小模式)
当我们把Canvas中的Render Mode设为Screen Space – Overlay 或Screen Space – Camera时,此Canvas Scale中的Ui Scale Mode(大小模式)就可用,且其中有3个选项:
1 Constan Pixel Size:固定像素尺寸
 
2 Scale With Screen Size:以宽度为标准缩放(屏幕自适应特性)
 
2.1  Reference Resolution:参考分辨率
    在不同分辨率下,控件显示的大小有所不同,这要根据实际情况综合考虑。
2.2  Screen Match Mode:屏幕匹配模式
Match Width Or Heigt:匹配宽度或高度
    此模式下会出现Match调节滑杆,调节其控块位置,也会影响UI元素显示的大小。
Expand:扩展
Shrink:收缩

3 Constant Physical Size:固定物理尺寸
 
三.Panel 面板
当我们初次创建Panel后,它会充满整个画布,如下左图:
   
此时通过拖动该面板控件的4个角点或四条边可调节面板的大小,如上右图
面板实际上就是一个容器,在其上可放置其他UI控件,当移动面板时,放在其中的UI组件就会跟随移动,这样我们可以更加合理与方便的移动与处理一组控件。也就是通过面板,我们可以把控件分组。一个功能完备的UI界面,往往会使用多个Panel容器控件。而且一个面板里还可套用其他面板。
当我们创建一个面板后,此面板会默认包含一个Image(Script)组件:
 
该组件中的Source Image是设置面板的图像。
Color,可改变面板的颜色。
四.EventSystem事件处理系统
当我们创建一个画布时,Unity系统会自动为我们创建一个EventSystem,
 
该事件处理器中有3个组件:
1. Event System:事件系统组件(事件)
2. Standalone Input Module:独立输入模块 (输入)
3. Touch Input Module:触控输入模块 (触控)
如果我们将Event System (Script) 前的勾去掉,则管理整个场景的事件系统则不起作用了,此时运行程序,如果有Button,单击它时就不会有反应了。
五.Text控件
在UGUI中,我们所创建的所有UI控件,它们都有一个UI控件特有的Rect Transform组件:
 
我们所创建的三维物体,是Transform,而UI控件是Rect Transform,它是UI控件的矩形方位,其中的 指的是UI控件在相应轴上的偏移量。
UI控件除了上面Rect Transform控件外,每个UI控件都还一个 组件,它是画布渲染,一般不用管它,因它不能点开的。
Text控件的相关属性:
Character:(字符)
Font:字体
Font Style:字体样式
Font Size:字体大小
Line Spacing:行间距(多行)
Rich Text:“富”文本。例如:U<b>G</b>U</i>I<volor=”yellow”>学</color>习
Color:字体颜色
Paragraph:(段落)
 :设置文本在Text框中的水平以及垂直方向上的对齐方式。
 :水平方向上溢出时的处理方式。它有两种:Wrap隐藏;Overflow溢出。
 :垂直方向上溢出时的处理方式。它有两种:Truncate截断;Overflow溢出。
隐藏了或截断了,信息显示不全当然不好,但如果溢出又会破坏版面,想两全齐美的话,就可选中: ,如果文字多时,它会自动缩小以适应文本框的大小,当选中该项后,在其下边会出现Min Size与Max Size两输入框,可设置字体变化时的最小与最大值。
六.Image控件
Image控件除了两个公共的组件Rect Transform与Canvas Renderer外,默认的情况下就只有一个Image(Script)组件:
 
Source Image是要显示的源图像,但如果我们把一个普通的图像往里拖放时,却不能成功放入,认真研究一下不难发现,放图像的框中,除了None表示还没有图像外,还有一个括号注释的Sprite,它的意思是精灵,可理解为它是贴图的一种特殊形式,它不具备其他功能,只给UI做显示图片用,故我们给它取了一个特殊的名字:精灵Sprite,所以在Unity4.6中,要想把一个图片赋给Image,则需要把该图片转换成精灵格式,转换方法为,在Project中选中要转换在图片,然后在Inspector检视图中,单击Texture Type(纹理类型)右边的下拉框,在弹出的菜单中,第三项Editor GUI and Legacy GUI是Unity4.6以前版本所用的,选中它时,图片不会被拉伸,现在几乎不用,是为了兼容,而第四项Sprite(2D and UI),就是4.6版本所用的,它虽然比前一项适用的范围更窄,但其效率更高。我们选中该选项Sprite(2D and UI)并点击下方的Apply(应用)按钮就可把此图片转换成精灵格式,随后就可拖放到Image的Source Imag中了,如下图所示:
 
另:当我们把一个普通的图片转换成精灵格式后,在Project中,将发现该图片将包含一个子对象如图: ,以后可以把一个图片划分为多个图片。
当我们把精灵图片赋给了Image后,其组件样式如下图:
 
Color:可改变图片的颜色;
Material:材质,这是针对一些复杂的贴图使用。
Image Type:贴图的类型,这是最重要的属性。
   
1) Simple:简单
 
Preserve Aspect:翻译过来是:维持外貌,选中该项后,该精灵图片的长宽比将保持原状,当调节图片的大小时,它将在保持原长宽比的前提下尽量铺展到图片框中,即不会拉伸或压缩以适应图片框而铺满。
Set Native Size:本来的大小。如果调整后大小变乱了,单击此按钮,可将此图片设置成本来的大小
2) Sliced:片
应用该种类型时,应先将贴图进行“九宫格”处理后才可以应用。否则其下会出现黄色的警告  This Image doesn’t have a border:这个图片没有边框,如下图:
 
            怎样进行九宫格的处理呢?
            先在Project中选中该图片精灵,然后在其Inspector检视图中单击“Sprite Editor”按钮即可进入九宫格处理Sprinte Editor视窗中,如下两个图所示:
   
在这个Sprinte Editor视窗中,我们可以拖动图像四条边上的绿色线,调节九宫格的布局大小,调好后单击顶端的Apply按钮应用即可,回到Image的检视图中,我们将发现原来的警告消失了:
 
在Image Type为Sliced时,当对图像进行大小调节时,其中心会缩放以适合矩形,但边界会保持不变,这样当你显示不同尺度图像时,不用担心扩大与缩小时其轮廓会发生变化。如果你只想要边界,不要中心,可以禁用Fill Center(填充中心)属性。
3) Tiled:平铺
 
图像保持其原始大小,重复多次填补空白。这往往用于做背景。
4) Filled:填充
图像填充满整个Image矩形区域,再结合Fill Amount属性,可做一些特效。

当图片类型为Filled时,其Image组件的属性视图如上图所示,其Fill Method(填充方法)选择框中有5种:
Horizontal:水平填充,如果我们手动拖动Fill Amount(填充数量)滑块,就可看到图片在水平方向上的填充变化(动画),如下图列所示:
     
Vertical:垂直填充,同理当我们手动拖动Fill Amount滑块,就可看到图片在垂直方向上的填充变化(动画),如下图列所示:
     
Radial 90:径向90度填充,同理当我们手动拖动Fill Amount滑块,就可看到图片在90度方向上的填充变化(动画),如下图列所示,默认是以左下角为圆心,顺时针90度填充。
     
Radial 180:径向180度填充,同理当我们手动拖动Fill Amount滑块,就可看到图片在180度方向上的填充变化(动画),如下图列所示,默认是以底边中点为圆心,顺时针180度填充。
     
Radial 300:径向180度填充,同理当我们手动拖动Fill Amount滑块,就可看到图片在360度方向上的填充变化(动画),如下图列所示,默认是以图片的中心点为圆心,顺时针360度填充。

如果我们使用脚本来控制Fill Amount的值,就可制造出这五种动画来,为特效的制作增加了一些有效的手段。

七.Button控件
当我们创建一个Button后,其Inspector视图如下:
 
除了公共的Rect Transform与Canvas Renderer两个UI组件外,Button还默认拥有Image(Script)与Button(Script)两个组件。
组件Image(Script)里的属性与前面所讲的Image控件的Image(Script)组件里的属性是一样的,例如Source Image的图像类型仍为一个Sprite(精灵),通过为此赋值,就可改变此Button的外观了,如果你为属性赋值了图片精灵,那么此Button的外观就与此精灵一致了。
Button是一个复合控件,它中还包含一个Text子控件: ,通过此子控件可设置Button上显示的文字的内容、字体、样式、字大小、颜色等,与前面所讲的Text控件是一样的。
Button(Script)组件里的属性:
 
1 Interactable:是否启用(交互性)
如果你把其后的对勾 去掉,此Button在运行时将点不动,即失去交互性了。
2 Transition:过渡方式
它有四个选项 ,默认为Color Tint(颜色色彩)
1) None:没有过渡方式。
2) Color Tint:颜色过渡    
Target Graphic:目标图像
Normal Color:正常颜色
Highlighted Color:经过高亮色
Pressed Color:点击色
Disabled Color:禁用色
Color Multiplier:颜色倍数
Fade Duration:变化过程时间
3) Sprite Swap:精灵交换。需要使用相同功能不同状态的贴图。
 
Target Graphic:目标图像
Highlighted Sprite:鼠标经过时的贴图
Pressed Sprite:点击时的贴图
Disabled Sprite:禁用时的贴图

4) Animation:动画。最复杂,效果最绚丽。
 
其中的Normal Trigger、Highlighted Trigger、Pressed Trigger、Disabled Trigger等属性是不能赋值的,它们是自动生成的。
当单击“Auto Generate Animation”(自动生成动画)按钮时,系统会为你打开一个New Animation Contoller(新建动画控制器)窗口,要求你选择动画存放的路径,所以我们要先在Project中新建一个文件夹,专门用来存放动画,比如此文件夹取名为_Animator,此时就可选中此文件夹,并给此动画取名(动画的名默认为该Button的名字,当然其扩展名为controller),创建成功后,会在Project中的_Animator文件夹中可看到刚才创建的动画文件(动画的名默认为该Button的名字),且在这个Button的Inspector检视图中可看到会为此Button增加一个Animator组件:
 
此组件的Controller的属性值就为刚才创建的动画,双击它即可打开该动画的Animator窗口,其中记录的有四个动画:Normal、Highlighted、Pressed、Disabled(通常状态、鼠标经过状态、按下状态、失效状态),如下图所示:
 
其实这个动画还没有,要做出这个动画,需先选中这个Button,然后点击系统菜单Window->Animation(注意不是Animator),就会打开一个Animation动画编辑窗口:
 
我们以此工具先来做一个简单的帧动画,在帧数框上单击一下,原来的0便会选中,此时输入数字1,表示对第1帧做动画,此时的录制按钮 以及系统的播放按钮 和动画编辑窗中的帧数线均会变红,表示现在处于记录模式,你所做的操作会记录成动画的动作,如下图所示:
 
假设我们想使它在第一帧时,该按钮变大一点,我们就在Inspector中的Rect Transformr的Scale中操作,比如在x与y轴向上均增大到原来的1.05倍(因UI是在x-y平面上显示,故不需设置Z值),如下图所示:
 
同理,当第一帧设置完后,我们在帧数框中直接输入数字2,就可编辑第2帧的动画了,比如在x与y轴向上均增大到原来的1.1倍,再继续设置第3帧在x与y轴向上均增大到原来的1.15倍,假设我们现在就只做这3帧动画,已经完成了,那么我们点击一下那个处于红色晕光状态下的录制按钮 ,结束录制,再保存一下场景,就会发现在Project视图中_Animator/Button1下会增加4个动画文件Disabled、Highlighted、Normal、Pressed(如果不保存将看不到这4个动画文件),这四个动画剪辑就是源于我们刚才的录制,虽然刚才我们只录制了一次,而系统会为我们自动产生这四个动画文件。
动画成功制作出来了,可运行起来看看效果了,但当我们运行起来时,就会发现那个按钮会自动无限次地播放那个动画,看起来就是不断地颤动,那么是什么原因呢?
我们在Project视图中,选中任意的一个动画剪辑比如Disabled,在其Inspector视图中可看到它的Loop Time属性是处于选中状态,意思是循环播放,那么我们把它去掉即可。
 
而这里,如果我们只去掉Disabled的Loop Time还是不行的,当然把这4个动画剪辑的Loop Time全去掉是能成功的,那么到底是那个剪辑在起作用呢?实际上是Normal,所以我们只需去掉Normal的Loop Time即可。其原因请看下列叙述。
我们先在Hierarchy视图中选中那个做了动画的Button,再单击系统菜单Window->Animation(注意不是Animator),就会打开刚才的动画编辑窗口:
 
单击其中的Normal框,会出现下拉选择:
 
    从这下拉选择列表中可看出Normal前边有个勾,这说明我们刚才所做的动画是Normal。那么根据此原理,我们可分别做出鼠标经过该Button的Highlighted、该Button按下的Pressed、以及该Button失效的Disabled动画了。
3 Navigation
 
None:没有
Horizontal:水平
Vertical:垂直
Automatic:自动
Explicit:明确的
4 Visualize:显现
5 On Click()
在Button组件的下方有一个OnClick()选项,这就是Button控件处理事件的重要机制。
OnClick()意思为当该按钮被点击时所发生的事件,而此事件在UI中是委托机制,要理解这个机制,我们先做一些准备工作。
1) 建立脚本文件
假设此脚本文件被命名为ButtonEvent
using UnityEngine;
using System.Collections;
public class ButtonEvent : MonoBehaviour {
// Use this for initialization
void Start () {
}

// Update is called once per frame
void Update () {
}
}
这是系统默认的文件内容,现在我们要把它应用于UI,故必须引入UI的命名空间,即脚本的首部增加一行:using UnityEngine.UI;。
假设我们单击一个按钮后,让系统在后台显示一句话:点击了Button!,那么我们可以在此脚本中增加一个方法,该方法为公共的public,假设方法名为DisplayInfo:
Public void DisplayInfo(){
   Print(“点击了Button!”);
}
此时整个脚本文件的内容如下:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class ButtonEvent : MonoBehaviour {
// Use this for initialization
void Start () {
}

Public void DisplayInfo(){
   Print(“点击了Button!”);
}

// Update is called once per frame
void Update () {
}
}

2) 在Hierarchy视图的Canvas中创建一个空对象,并假设命名为Event,并把上面的脚本作为组件挂到这个空对象上,那么这个对象是具有事件处理能力的object了。
3) 为某个按钮添加其事件处理的委托对象
我们在层级面板中选中要产生单击事件的按钮比如Button1,然后拖动其Inspector面板右边的滚动条,使其Button(Script)组件下的OnClick()显现出来:
 
此时其事件列表为空:List is Empty,我们单击其下的“+”按钮为其添加一个事件:
 
此时事件虽被添加了,但其委托的事件处理对象为空:None(Object),当然连事件处理对象都没有,其事件处理方法自然也就为空:No Function。(Runtime Only此项我们可先不管它,以后用到时再讲)
那么怎样委托事件处理对象与选择事件处理方法呢?
很简单,我们把层级面板中刚才建好的并已挂上了事件处理脚本的Event对象拖放到None(Object)框中即可,此时此框中显示的内容即为委托的此事件处理对象的名称了:Event,有了事件处理对象了,然后使用该对象的什么方法来处理事件呢?这还需我们给它指定,其方法是单击显示内容为No Function的那个事件方法框,会弹出菜单列表:
 
当我们的鼠标指向最后一项ButtonEvent时会继续展开,其中就有我们在脚本中编写的事件处理方法:DiaplayInfo(),选中它即可,这样就完成了事件的委托,当我们运行时,单击那个Button,就会在后台里打印出“点击了Button!”。
一个按钮可以有多个处理事件。
下面我们采用另一种显示信息的方式。
我们先在场景中的画布上增加一个Text控件,同时设置好相关的显示样式,然后在那个脚本中增加一个公共变量:public Text Txt_Info,回到场景视图中,在层级面板中选中挂有该脚本的对象Event,在其Inspector视图的ButtonEvent脚本组件里就会出现刚才增设的公共变量名:Txt_Info(脚本需保存且界面需刷新),我们可把增加的那个Text拖到此处即可为此变量赋值了,接下来我们就可通过脚本修改这个文本框控件的Text属性,让打印在后台的信息显示在这个文本框上了。其脚本文件内容如下:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class ButtonEvent : MonoBehaviour {
public Text Txt_Info;
// Use this for initialization
void Start () {
}

// Update is called once per frame
void Update () {
}

public void DisplayInfo(){
    print ("点击了Button!");
}
public void DisplayInfoText (){
    Txt_Info.text="Button被点击!";
}
}
然后再给那个Button增加一个单击事件(委托的事件对象仍为Event,方法则设为DisplayInfoText ()):
 
再次运行程序,单击按钮时,后台输出“点击了Button!”的同时,场景中的Text文本框的内容内变为:Button被点击了!。
八 Anchor锚点与屏幕自适应
每个控件都有下Anchor的属性,其作用是当改变屏幕分辨率的时候,当前控件做如何的位置变换。即控件的屏幕自适应。
当我们创建一个Canvas后,在层级视图中选中它后,我们将发现这个Canvas在场景视图中的样式如下图所示:
 
这个Canvas除四边、四个角点外,其中心还有一个蓝色的小圆圈,这个小圆圈即为这个Canvas的中心点。
如果此时我们在此Canvas上创建一个Button,如下图所示:
 
Button同理也有四条边、四个角点、一个中心点,此时我们选中Button,在Canvas中心点位置会出现一个小雪花图案 ,这就是Button在Canvas上的锚点,可用鼠标拖动它,且拖动的过程中会实时显示此锚点距上、下、左、右的百分比,如下图所示:
 
而且在Button的Inspector面板中,单击Rect Transform中 按钮,会打开一个Anchor presets锚点调整窗,如下图所示:
 
如果我们单击  时,其锚点就会跑到Canvas的右上角,如下图所示:
 
同理单击其他位置时,也就把锚点调整到相应的位置上,即可把此锚点调整到画布的中心点上、四个角上、四条边的中点上。
以上是锚点整体移位。实际上,我们还可以拖动小雪花中的任意一个花瓣,使其分散成四个锚点,如下图所示:
 
当我们单击 这个窗中的最右边或最下边中一些按钮可将锚点分散在两边或上下或四个角点上。
原来这个雪花状的锚点是由四个锚点组成的复合体。
说了这么多,那它到底有什么用途呢?
两个字,定位。
如图: ,当屏幕大小发生改变时,Button的四个角点与对应的4个锚点的距离会保持不变,从而保证布局不会随着屏幕大小或者分辨率的改变而改变,可相对有效地保证布局不会混乱,以达到屏幕自适应的目的。
但要注意,控件的锚点总是相对于自己的上一级来定义的。例如,我们再在这个Canmas中创建一个Panel并调小它的大小,在Panel中创建一个Text,我们去调节这个Text的锚点时我们将会发现这个锚点总是相对于Text的上一级Panel来定义与变换位置的。
九、应用:登录界面
在画布上:
1、 两个Text控件,分别命名为:Tex_UserName与txt_Password,其Text值分别为“用户名称:”与“登录密码:”
2、 两个InputField控件,分别命名为:Inp_UserName与Inp_Password。
这两个输入域控件是前面还没有介绍的新控件,在其层级Hierarchy视图将其展开 ,可发现它也为一个复合控件,在主控件上还包含两个子控件,一个为Placeholder与Text,其Text就是前面所介绍的文本控件,程序运行时用户所输入的内容就保存在这个Text中,而Placeholder是占位符,它表示程序运行时在用户还没有输入内容时显示给用户的提示信息,在这里我们把它设置为“请输入…”,设置方法是在层级视图中展开这个InputField控件,选中其子控件Placeholder,在Inspecter视图中可发现其Text(Sript)组件,修改其值为“请输入…”即可,如下图所示:
 
InputField控件与其他控件一样,也有Image(Script)组件,自身组件InputField(script)中也有变换Transition属性,其默认值也为颜色变换,除此之外,它有一个重要的属性:ContentType(内容类型),有10个选项,如下图:
 
1) Standard:标准的
2) Autocorrected:自动修正
3) Integer Number:整数
4) Decimal Number:十进制小数
5) Alphanumeric:字母数字
6) Name:人名
7) Email Address:邮箱
8) Password:密码
9) Pin:
10) Custom:定制的
据此,我们把第一个InputField的内容类型设为第5个Alphanumeric:字母数字,第二个InputField的内容类型设为第8个Password:密码,这样程序就可启用其自动验证功能,例如在用户名称输入框中如果你输入的不是字母或数字则不能输入进去,第二个密码框中输入密码时它会默认以*号占位输入的密码。
    3、创建一个Text,用于显示登录是否成功等提示信息用,在其Inspector视图中,去掉其默认显示的内容,即让其才开始运行时不显示任何内容,并把其Best Fit勾选上,让其提示信息自适应文本框的大小:
 
4、最后创建一个提交按钮,其整个界面如下图所示:
 
5、创建脚本(红色部分为新增内容):
using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class LogonSystem : MonoBehaviour {
public Text Txt_DisplayInfo;
public InputField Inp_Username;
public InputField Inp_Password;

// Use this for initialization
void Start () {

}

public void DisplayInfo(){
string strUserName = Inp_Username.text;
string strPW = Inp_Password.text;
if(CheckLogonInfo(strUserName,strPW)){
    Txt_DisplayInfo.text="登录成功!";
}else{
    Txt_DisplayInfo.text="用户名称或登录密码错误 请重新输入!";
}
}

private bool CheckLogonInfo(string strUserName,string strPW){
bool ReturnFlag = false;
if (strUserName != null && strPW != null) {
if(strUserName.Trim().Equals("Admin") && strPW.Trim().Equals("123456")){//两个输入框中的内容去掉前后空格,如果用户名称为Admin、登录密码为123456时,则表示登录,将ReturnFlag标记为真true。
     ReturnFlag=true;
    }
}
return ReturnFlag;
}
}
脚本创建好了后,我们在Hierarchy中创建一个空对象,命名为_LogonSystemEvent,对把这个脚本挂在它身上,而且在其Inspector视图中,为其3个公共变量赋值(把相应控件拖放到对应变量的值框中):
 
运行程序,如果用户名称或密码错误,提示信息框中会显示:用户名称或登录密码错误 请重新输入!,否则显示:登录成功!,如下图所示:
   
十、Toggle控件动态事件
Toggle:开关,当我们创建它后 ,可发现它也为一个复合型控件 ,它有Background与Label两个子控件,而Background控件中还有一个Checkmark子子控件,如果我们将其拖散 可清楚地看见,Background 是一个图像控件,而其子控件Checkmark 也是一个图像控件,其Label控件 是一个文本框,它们与我们所讲的控件是一致的,我们通过改变它们所拥有的属性值,即可改变Toggle的外观,如颜色、字体等等。下面来看看Toggle的一些重要属性。
1 Is On:目前是处于开还是关。
    当我们运行起来,用鼠标点击那个Toggle按钮,将发现其中的对勾符号会在出现与不出现之间切换,同时与之相对应的,在其Inspector面板中,属性In On后面的对勾也在勾选 与不勾选 之间切换。
2 Graphic:图像
实际上,用鼠标点击那个Toggle按钮,其对勾符号会在出现与不出现之间切换,它的原理就是控制那个对勾图像出现与不出现而实现的,这个Graphic就是设置这个属性值的,你可点击 最后那个设置按钮 ,在出现的窗体中 选择另外的图像如Background,同时将 中的Target Graphic的值设为Checkmark,即将它们两者的值互换,将发现,当我们点击Toggle按钮时,其对勾不会消失与出现,而是背景消失 与出现 。这样做虽然没有多大实用价值,但说明Unity可以方便地控制这个按钮的外观。
3 Group:组(单选框功能)
在Hierarchy面板中,选中我们刚才创建的Toggle,然后按键盘Ctrl+D两次,就可复制出两个Toggle了,并在场景视图中拖动它们的位置,使它们都可见,运行,我们将发现这个三个都可选中,即它们是复选框。
那么怎样做出单选的效果呢?
前面所创建的按钮是独立的,互不关联,当然就可独自地选与不选。如果我们把这三个组成一个组,让它们关联,就可做成单选了。
 
从Group属性可看出它需要一个ToggleGroup。
我们先在画布上建立一个空对象,并命名为_ToggleGroup,在其Inspector中单击 这个按钮,为其添加组件,在弹出的菜单中选择UI,在后续弹出菜单中
   
选择“Toggle Group”,这样我们就为此对象添加了ToggleGroup组件了。
在Hierarchy中同时选中要成组的那3个Toggle,把已添加了ToggleGroup组件的_ToggleGroup拖到Inspector的 中即可,这样我们便把这3个Toggle成组了,于是它们3个就只能单选其中一个了。
为了更完美,首先调整空对象_ToggleGroup的位置与大小,让其包含那3个Toggle控件,然后在Hierarchy中,把3个Toggle选中并拖到_ToggleGroup中成为子物体 ,这样在逻辑上与外观上均完备,且移动父物体时子物体也会跟着移动。

4 Toggle控件动态事件On Value Changed(Boolean)
 这是Toggle的事件处理,它与Button的事件有所不同,下面来看看怎样给Toggle添加事件。
我们把Toggle的标签改成装备名称,再在场景中增加一个显示选取的装备名的文本框:
 
编写脚本:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class _ToggleEvent : MonoBehaviour {
public Text Display;
private string info;
// Use this for initialization
void Start () {

}

public void Toggle1(bool isclik){
if (isclik) {
  info="屠龙宝刀";
  Display.text=info;
  }
}
public void Toggle2(bool isclik){
if (isclik) {
  info="倚天剑";
  Display.text=info;
  }
}
public void Toggle3(bool isclik){
if (isclik) {
  info="降龙十八掌";
  Display.text=info;
 }
}
}
在Canvas中创建一个空对象,把此脚本挂接到此对象上,并把场景中的Text赋给public Text Display这个变量,然后分别选中那3个Toggle,在其检视图中单击事件下的“+”号: ,单击后就为该控件添加了事件,如下图:
 
然而,事件要委托给一对象,现在还没有,显示的是 ,此时把已经挂接上了脚本的那个空对象拖放到此处,便委托事件处理对象了,如下图所示:
 
有了事件处理对象,但还没有指定方法,此时单击 会出现下图所示的菜单:
 
鼠标指向我们为此对象所挂的脚本名_ToggleEvent时就会继续展开一个菜单,其中就有我们在此脚本中编写的方法Toggle1(bool)、Toggle2(bool)、Toggle3(bool)。这个菜单与Button的有所不同了,Button的如下图所示:
 
比较这两个菜单可发现Toggle多了上面的那部分:
 
下面的 是静态方法,上面的 是动态事件,是系统自动生成,其bool型参数已被封装在其中了,此时我们不能象Button那样去选择其静态方法了,而要选择与之对应的动态事件才可正常运行了。
Toggle与Button在事件处理上的的区别不只是上面所谈到的那一点,还有其他区别,如:
Button事件组件面板的上部显示的是 
Toggle事件组件面板的上部显示的是 
Button是当Button被单击时发生,Toggle是当Toggle选中与不选中(即其值发生改变)时发生,且还有一个布尔型参数,选中时传进给参数的值为真,反之为假,所以在前面脚本编写中,其方法里我们要设置对应的布尔型参数来接受这个值:
public void Toggle1(bool isclik){
if (isclik) {
  info="屠龙宝刀";
  Display.text=info;
  }
}
根据上面的分析可知,Toggle选中与取消选中时都会产生事件,那么在一组单选按钮组中,当我们点选另一个按钮同时会取消前一个按钮的选择,那么这两个按钮就都会产生事件,为了证明这一点,我们将前面的脚本稍作改变:
public void Toggle1(bool isclik){
print("屠龙宝刀");
if (isclik) {
  info="屠龙宝刀";
  Display.text=info;
  }
}
public void Toggle2(bool isclik){
print(倚天剑");
if (isclik) {
  info="倚天剑";
  Display.text=info;
  }
}
public void Toggle3(bool isclik){
print(“降龙十八掌");
if (isclik) {
  info="降龙十八掌";
  Display.text=info;
 }
}
以前的代码是当选中即isclik为真时才显示相关信息,而取消选中,虽然事件也产生,但因isclik为假,所以不会显示信息,所以我们便觉察不出来,而现在加上了一个print语句,这样只要事件发生了,就都会在后台里打印出相关信息来,如下图所示:
 
这是首次运行选中“屠龙宝刀”时的情况。
 
接着我们选中“倚天剑”时,“屠龙宝刀”也被取消,“倚天剑”与“屠龙宝刀”都产生了事件,所以会打印出各自的信息。
 
再在这基础上选中“降龙十八掌”,“倚天剑”被取消,同时“倚天剑”与“降龙十八掌”均产生了事件,所以倚天剑与降龙十八掌相关信息都会打印出来。
以上就是Toggle的事件处理机制,如果我们Toggle不是单选而是复选,那么我们又怎样在那个文本框中显示出多选信息呢?
这个问题看似很难,实际上很简单,代码如下:
public void Toggle1(bool isclik){
if (isclik) {
  info += "屠龙宝刀";
  Display.text=info;
  }
}
public void Toggle2(bool isclik){
if (isclik) {
  info += "倚天剑";
  Display.text=info;
  }
}
public void Toggle3(bool isclik){
if (isclik) {
  info += "降龙十八掌";
  Display.text=info;
 }
}
把info的赋值符号=变成+=,这样就把info的值累加起来了,也就记录下了多选值了。这虽然简单,但不完善,如果先选中,然后又去掉,但先添加进去的值却不会去掉,这需要进一步地处理才行。
5 事件运行模式
在事件组件面板中,第一个选项框中的值我们在Button时就是一直使用的是其默认值:Runtime Only,如下图所示:
 
当我们单击该选项时,出现的下拉菜单如下:
 
Off:关闭事件处理功能;
Editor And Runtime:编辑与运行时,其事件处理功能均起作用;
Runtime Only:仅在运行时,其事件处理功能才起作用。

我们假设把Toggle1方法改成如下所示:
public void Toggle1(bool isclik){
print ("屠龙宝刀");
info = "屠龙宝刀";
Display.text=info;
}
然后把第一个Toggle的事件处理对象及方法置空,并把事件运行模式变为Editor And Runtime,此时再为它添加事件对象及方法,我们将发现在添加结束后,其事件便立即发挥作用了,如下图所示,当然程序运行时也能发挥其功效的,这就是“编辑与运行时,其事件处理功能均起作用”的意思。
 
    此功能可以为我们程序员提供预览、事前先知的功能,方便我们编辑。
十一、Slider控件(滑动条)
1、 Slider也是一个复合控件 ,Background是背景,默认颜色是白色,我们保持不变;Fill Area是填充区域,其子控件Fill中只有一个Image(Script)专有组件,假设我们将其颜色改为红色;Handle Slice Area中的子控件Handle(手柄)中也只有一个Image(Script)专有组件,假设我们将其颜色改为黄色,那么Slider的外观为: 
2、 当我们在Hierarchy中选中Slider控件,其Inspector中的Slider(Script)属性面板如下图所示:
 
上部的Interactable、Transition与前面介绍的控件是差不多的,下面谈谈它特有的一些属性:
Fill Rect:填充矩形区域
Handle Rect:手柄矩形区域
Direction:Slider的摆放方向,可以从左到右、从右到左、从上到下、从下到上
Min Value:最小数值
Max Value:最大数值
Whole Numbers:整数数值。假设我们将Min Value设为1,Max Value设为100,那么调节手柄时,对应的值在1到100之间,而且是一个小数,如55.67,有时我们希望它是整数,那么选中该项即可。
上面的后4项Direction、Min Value、Max Value、Whole Numbers都比较好理解,而前2项Fill Rect与Handle Rect较困难,下面我们做一个实验便可知其意义了。
我们再创建一个Slider控件并命名为Slider2,并把其所有子控件的名字改成在其默认名后加2的名字,再把先前的那个Slider改名为Slider1,也把其所有子控件的名字改成在其默认名后加1的名字,如下图:
 
此时我们先选中Slider2,然后把Slider1的子子控件Fill1拖到Slider2的Fill Rect中,如图:
 
运行程序,当我们调节Slider2的手柄时,我们将发现Slider1的填充区域在发生变化,为什么呢?因为默认情况下Slider2的Fill Rect是它自己的Fill2,而我们现在把Slider1的Fill1赋给了它,作为它现在的Fill Rect对象,所以就会出现此种情况,这就是Fill Rect的功效。
在上面的实验中,我们把Slider1用来做显示,Slider2用来控制,如果能把Slider1中黄色手柄隐藏起来就更好了:
 
这也是能做到的,我们选中Slider1的子子控件Handle1,在其Inspector视图中,将其Image(Script)前面的对勾去掉 ,这样就不会显示那个手柄了:
 
3、Slider的动态事件
假设我们想在拖动手柄的时候,让其值显示在右边的一个文本框中,如下图所示:
 
这就要用到Slider的动态事件了 
 指的是Slider的滑块滑动其值发生改变时而产生的动态事件,它有一个参数Single:单精度,实际上这里指的是float,整个事件的机理是,当滑动滑块时,其值发生改变,事件产生,而且会实时将滑块所对应的值传给此事件,保存在这个参数中,供程序使用。
根据此原理,我们建立下列脚本SliderDemo:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class SliderDemo : MonoBehaviour {
public Text Txt_diaplayValues;
// Use this for initialization
void Start () {

}

public void DisplaySliderNumber(float floValues){
Txt_diaplayValues.text = floValues.ToString ();
}
}
然后在场景中创建一个空对象,把此脚本作为组件挂在这个空对象上,再在场景中创建一个文本框,并赋给 public Text Txt_diaplayValues变量。
最后一步就是注册事件了,单击 中的“+”按钮,为其添加事件 ,把前面创建的并已挂上脚本的那个空对象拖到 中为其委托事件处理对象 ,再指定事件方法,单击 :
 
选择脚本名SliderDemo中的方法DisplaySliderNumber,不过这里不能选择DisplaySliderNumber(float),而要选上面的DisplaySliderNumber动态事件(其原因请见前一节的叙述)。
运行程序就可达到要求了。
十二、ScrollBar控件(滚动条)
 
其属性: 与前面的Slider差不多,动态事件也是一样的,这里就只谈其特有的属性:
Value:当拖支滑块时,其值是在0到1之间变化的
Size:是滑块的大小,如果把它改成0.5,滑块就会大到占滑条的一半大。
Number Of Steps:数值的步骤。假设设定为5,那么就会把Value分成5个值,调节滑块时其值就在这5个值中变化。
十三、一个简单的游戏主界面
 
右边蓝底色中的“生化危机”为Text;开始、设置、结束为Button,并加上简单的帧动画Highlighted(见前面的动画过渡)
左边淡黄底色区域为面板panel,它为父控件,其中所包含的所有控件均为它的子控件,游戏难度下的三个选择钮为单选按钮组,其设置方法请参见前面Group。
这个面板,通常情况下是不可见的,当单击“设置”按钮时面板从左外侧滑入而可见,进行设置后单击面板中的“确定”按钮后,面板又滑出到左外侧处于不可见状态。这是一个动画,其设置方法与Button动画相似,但它不是利用Button那种自动生成动画的方法而建立的。
下面我们先来建立面板的动画过程:
首先,在Hierarchy面板中选中要建立动画的Panel,单击主菜单Window中的Animation(注意不是Animator)出现Animation窗口:
 
此时单击顶端最左侧红色按钮,它为动画录制按钮,会弹出一个对话框,要求为将要录制的动画选择保存位置与动画的名称,如下图:
 
假设我们将此动画保存在Assets下的_Animation文件夹中,并取名为ShowPanel.anim,单击保存后,其界面如下:
 
录制的动画是面板从不可见到可见的显示动画,那么在第0帧时面板是不见的,所以此时把面板水平向左拖动到画布的左外侧,如下图所示:
 动画录制机会记录下此时对象的位置,其Inspector面板中的RectTransform参数会发生改变:
 ,即在第0帧时,面板在X:-140,Y:-1.3751e-05处。
接下来我们把帧数框中的帧数改为60帧,即设置第60帧时面板的位置(设为60帧的原因是,面板从第0帧的位置变化到第60帧的位置时其动画时间大约为1秒钟),此时录制面板中的那条红色竖线将移动到1:00处,如下图:
 
根据动画设计要求,此时面板应该是完全可见的,所以我们把面板水平向右拖到画布中,动画录制机同样会记录下此时面板的位置 ,其设置窗口如下图:
 
我们还可发现,在现在位置的红线处会出现象0帧处的关键帧图标: 。
我们在 上单击一下,其展开图如下:
 展开的那两项分别表示在此过程中,对象在X轴上位移了131,Y轴上为0。
从底部的 处可见,现在的视图是Dope Sheet(关键帧清单),当点击Curves(曲线)时,会转换为曲线视图,如下图
 
如果曲线过大显示不全或过小看不清时,可调节鼠标中键滑轮,缩放曲线到适当大小。
图中水平绿色线 所包含的区域即为动画区域,曲线上的红色点为关键帧点,可看出此动画只有两个关键帧,用鼠标单击关键帧时,Scene视图中的面板就会移动到所设置的位置上。
设置好后,单击红色的录制按钮一下,即可停止录制且保存动画。
这是显示面板的动画,下面我们来设计隐藏面板的动画。
单击已经显示出刚才录制的动画名为ShowPanel这个框右边的按钮,弹出:
 
单击[Create New Clip],创建一个新的动画剪辑,此时会同样弹出一个要求你为新动画取名以及选择保存路径的对话窗,假设我们为此动画取名为HidePanel.anim,路径与前一个动画相同,其后的动画设计与前边一样,只不过第0帧时,面板在画布上,第60帧时,面板在画布外。
动画创建结束后,即可直接关闭录制窗体,系统会自动保存动画文件。回到系统界面下,在Project视图中的我们事先设定的保存动画的_Animator文件夹下将会新增三个文件: ,而且在Hierarchy视图中选中已设置了动画的控件Panel,在其Inspector视图中将发现系统会为其自动增加动画组件Animator。
好了,万事具备,运行程序,但发现那个面板的显示动画会不断地重复播放,其原因是其Loop Time的属性值为True,如果设为False即可去掉其默认的自动播放特性。方法为,在Project视图中选中 动画,然后在其Inspector视图中,把 后的钩去掉即可(同理,其HidePanel动画也做这样的处理),但程序运行时,面板虽不会重复播放显示动画了,但它会播放一次,这与我们的设计初衷不符,我们的初衷是程序运行时不显示面板,当我们点击设置按钮后才会显示面板,点击面板中的确定按钮后,隐藏面板。要达到这种功能,就要设置动画状态机中的一些属性以及配合使用脚本才能完成。
首先,让动画不自动播放。
在Hierarchy视图中选中设置了动画的控件Panel,可发现其Inspector视图中Animator组件中的第一个属性Controller(控制器,也称动画状态机)的值为前边录制的 动画:Panel,如图: ,单击其后的设置按钮 可更改为其他动画,不过这里是不需更改的,我们双击动画框中的已有的动画Panel,会打开Animator窗口:
 
其中有两个按钮代表的就是按钮文字所指示的动画剪辑:ShowPane、HidePanel,即现在这个Panel动画里有两个动画剪辑。我们在面板的空白地方单击鼠标右键: ,选择第一项Create State中的Empty,创建一个空动画,其默认名为New State,如图:
 
在New State上点右键,在弹出的 中选第二项Set As Default,即设置这个空动画为默认播放的动画,既然为空动画,自然就不会播放动画,经此设置后的窗口如下图所示:
 
比较设置前后两图可发现,代表默认动画的按钮的颜色为黄红色。即哪个按钮为黄红色,那个动画剪辑就是当前动画播放时的状态位置,动画状态机的命名来源也许就是这个原因。
经此设置后,再次运行程序,就不会有动画播放了。但也许你会发现那个面板仍然显示出来了,出现这种情况,不是动画造成的,因现在虽然没有动画,但那个面板本来是存在的,当然要显示出来,如果想要它一开始就不显示,我们只需在场景编辑视图中将其位置拖到画布之外即可达到目的,如下图:
 
通常情况下,一个动画里有许多的动画剪辑,到底播放哪个动画,是动画状态机来决定的。 这个界面就是动画状态机界面,它表示现在的动画播放的是它中的哪个动画剪辑。那么,我们有什么办法来控制动画状态,或者说让动画在不同的剪辑中交替转换呢?
我们在“New State”上单击鼠标右键,选择第一项Make Transition(设置转换),然后直接移动鼠标,将会出现一个带前头的连线,再在”ShowPanel”上单击鼠标左键,即可在这两个动画剪辑上建立转换联接: ,这个连线表示,当满足某个条件时,就可由播放剪辑New State变成播放剪辑ShowPanel,即使ShowPanel处于播放状态。这个条件又在哪里设置呢?单击 右边的“+”按钮,在弹出的 中选Bool,即设置一个布尔型参数,并把此参数假设命名为IsDiaplay,如图 。
设置好参数后,用鼠标单击那根连接线,它将变成蓝色,表示选中,如图:
 
接下来,在其Inspector视图中的Conditions设置条件变量为刚才建立的IsDiaplay,条件值为true,如下图:
 
其意思为,当IsDiaplay为真true时,动画播放状态由New State转变为ShowPanel。
同理,可设置由HidePanel到ShowPanel转变条件也为当IsDiaplay为真true时。
 
这两步设置的结果我们可以这样理解,当IsDiaplay为真true时,动画播放状态处于New State或HidePanel时,都将转到去播放ShowPanel。
依此类推,当IsDiaplay为假false时,动画播放状态如果处于ShowPanel时,将转到去播放HidePanel。这样其动画状态我们就设定完了,如下图:
 
接下来我们利用脚本去改变那个变量的值,就可实时控制动画的播放了。
脚本内容如下(红色部分为增加的脚本内容):
using UnityEngine;
using System.Collections;
public class SettingPanelDisplay : MonoBehaviour {
public Animator Ani_SetPanel;
void Start () {
}
public void ShowPanel(){
Ani_SetPanel.SetBool ("IsDiaplay",true);
}
public void HidePanel(){
Ani_SetPanel.SetBool ("IsDiaplay",false);
}
}
在场景中创建一个空对象,把脚本挂在它身上,并在Inspector视图中把Hierarchy中的Panel赋给Ani_SetPanel,如图: 。
脚本中Ani_SetPanel变量是Animator类型,为何可以把控件Panel赋值给它呢?道理是Panel控件已经有了Animator组件,程序会自动抽取其中的Animator出来赋值给变量Ani_SetPanel。
Ani_SetPanel.SetBool ("IsDiaplay",true);其中的IsDiaplay就是在状态机中增设的参数,其拼写必须与状态机中的完全一致,否则不能运行。
最后设定“设置”与“确定”两个按钮的委托事件,具体设定过程请参见前面的Button事件。
至此,这个简单的游戏主界面便创建完工,可运行程序查看效果了。
十三、高级控件 滑动区域ScrollRect
滑动区域控件ScrollRect是在一个较小区域显示较多内部控件的一种机制。在UI系统中,这种控件的原型是没有的,它是我们开发者利用UI系统里已有的基本控件组合而成的。不过在UI系统里有ScrollRect这个类,即它是一个组件,不是控件。控件与组件有什么区别呢?简单地说,在一个控件里可以添加组件,如在Button上可添加Animator组件。有些控件在创建时,它会默认自带一些组件,如Panel会自带Image组件,如图: ,其带括号的Script就表示这个Image是组件,因在UI系统里有Image这个基本控件的,故加上一个带括号的Script来区别控件与组件,说明此处的Image是组件而不是控件。同理,当我们创建一个Button控件时,默认情况下我们可以在其Inspector视图里发现它带有Image与Button两个组件的,如下图:
 
因在UI系统里有Image、Button这些基本控件,这里加上(Script)说明这个Image、Button不是控件而是组件。
这好像有点混,怎么Button控件里含有Button组件呢?
实际上我们可以这样理解,组件是一个脚本,是一个类,控件是这个类的实例化对象,是一个具体实现,所以Button控件里含有Button组件就好理解了。再一个,类之间可以继承,所以一个控件可以包含多个不同类型的组件。
根据上面的叙述,ScrollRect是待开发者自己去建立的控件,我们姑且把它称作隐形控件。下面我们来创建这个控件。
1、 创建一个画布Canvas,在画布上创建两个空对象,其中一个命名为ScrollRect,另一个命名为Content,再创建一个Scrollbar。调整它们的大小与位置,大致如下:
 
2、 在Content上创建几个子对象,这里我们创建4个Button,并调整它们的位置,使它们平铺在Content中,同时设置Button的Image的SourceImage的值(挂上图片,普通图像挂不上,应转换成精灵,请参见前面的Image)。
 
3、 组装ScrollRect:3个组件2个属性
添加ScrollRect组件:选中空对象ScrollRect,在其Inspector视图中单击“AddComponent”按钮,选择UI中的Scroll Rect,这样便为这个空对象添加上了ScrollRect组件。此组件里有2个重要属性Content、Horezontal Scrollbar,其功能简单说是用滑动条的滑动去控制内容区域的移动。根据此原理我们把Hierarchy视图中的Content与Scrollbar分别拖挂到这两属性值框中即完成了对象的指定工作。试运行程序并拖动滑动条,发现Content中4个按钮图片确实能随着滑动条的移动而移动了,如图:
 
    但这还不完美,如果能隐藏多余的内容,只显示特定区域的内容,就象网页中的滚动条那样就好了,如下图所示:
 
要达到此功能,还要为已添加了ScrollRect组件的对象增添Image、Mask(遮罩)两个组件,其添加方法与上一致。
至此,组装ScrollRect的3个组件ScrollRect、Image、Mask ,2个属性:ScrollRect中的Content、ScrollRect中的Horezontal Scrollbar已完备,但当我们运行时,其效果仍然与前面相同,不能隐藏多余部分。如果在Hierarchy视图中我们把Content拖到ScrollRect上,使Content成为ScrollRect的子对象,就可达到我们想要的效果了。
 
其原因是,我们所添加的Image与Mask是在ScrollRect上,那么用图像去遮罩的对象是ScrollRect,所以我们要把Content作为ScrollRect的子对象才能达到此效果。
十四、高级控件 标签页面TabPage
          
如图,当我们点击顶部不同的标签时,下部的内容区域会显示对应的内容版面。
1、在画布上创建一个空对象,命名为Lable,创建一个Image,布局上Lable在上面,Image在下面,如下图:
 
2、在Lable中创建三个子控件,它们均为Toggle,命名为Toggle1、Toggle2、Toggle3,并调整它们的Background与Checkmark,使其看起来像按钮:
 
调整方法:
 
Background是背景,是未被选中时表现出来的图景。首先在场景视图中将其大小调大,使其与整个按钮形状一样大,然后在其Inspector视图中的Image组件里设置Source Image或Color属性值,这里为了简便,我们将其Color值设为较暗的灰度色,用于它未被选中时展现出来的颜色。
 
Checkmark是选中时表现出来的图景,默认是一个对钩,同理首先在场景视图中将其大小调大,使其与整个按钮形状一样大,这样一来,Checkmark与Background一样大,两个重叠起来了,当未选中时,表现出来的是Background的景象,选中后表现出来的是Checkmark的景象。对于Checkmark的调整,然后在其Inspector视图中的Image组件里设置Source Image或Color属性值,这里为了简便,将Image组件里的Source Image属性设为空None,即去掉那个对钩图像,并将其Color值设为较亮的灰度色,用于它被选中时展现出来的颜色。
这三个按钮状的Toggle按设计思路应该为单选,所以按以前所学的知识,需设置它们的Group属性值为Lable ,同时,对于它们的Is On属性,第一个Toggle的保持勾选,另两个去掉勾选,即使开始时,第一个处于默认选中状态。
3、在Image上创建三个为空的子控件,分别命名为Page1、Page2、Page3,并调整它们的大小,使其与Image一样大,位置上与Image重叠。再在Page1、Page2、Page3上各自创建一个Text子控件,也调整它们的大小大致与Image相当,且位置上也与Image重叠,并分别输入要显示的文本内容。这三个Text是重叠在一起的,显示时,其内容也会重叠,如果我们只勾选第一个Text的父控件Page1的 ,另两个去掉勾选,那么显示时会默认显示第一个,另两个处于未激活状态,不会显示出来,自然就不会发生显示的重叠了。
4、最后一步我们来实现当我们点击顶部不同的标签时,下部的内容区域会显示对应的内容版面。
也许你会认为这要用到脚本,实际上有更简便的方法,当然会离不开事件处理机制的。下面以Toggle1为例,选中它,在其Inspetctor视图中,单击:
 
中的“+”号,为其添加事件,如下图:
 
委托事件处理对象,这里我们把Page1拖给 如下图:
 
指定事件处理方法:
 
虽然我们没有编写自己的脚本,但Unity有内置的许多方法的,这里我们选择第二项GameObject:
 
选择上边的动态方法:SetActive,设置结果如下图:
 
这个事件的运行机理是:当Toggle1的选中状态发生改变时(注意是状态发生改变,选中时会产生事件,那么由选中到取消选中,也会产生事件),所挂接的对象Page1会被激活或失效。
根据这个原理,Toggle2的事件委托对象为Page2,Toggle3的事件委托对象为Page3,这样就实现了当我们点击顶部不同的标签时,下部的内容区域会显示对应的内容版面了,如下图:
   
十五、大型游戏案例UI开发
 
 
1、结构搭建
在Project中建立一些文件夹
 
我们这里先把一些贴图导进Sprits中,并把它们转化为Sprint,其转化方法请参见前面的Image控件中的图像精灵。
2、配置Canvas参数
在Hierarchy中创建UI画布Canvas,其Inspector中的Canvas、Canvas Scaler(Script)设置如下:
Canvas:
 
Render Mode:渲染模式设为Screen Space – Came,这种模式可在画布与摄像机之间放三维物体,做出更绚丽的界面。
Render Camera:既然渲染模式为Screen Space – Came,那么在此处就需为此设置渲染所用的摄像机了,我们把Hierarchy中的主摄像机更名为UICamera后并拖到此处,为此指定渲染所用的摄像机。
Plane Distance:直译过来为“平面距离”,这里表示的是画布距摄像机的距离,默认为100个单位,虽然距离越大,在画布与摄像机之间所放的三维物体越多,但一般设成200个单位,这里我们设成200。
Canvas Scaler(Script):
 
Ui Scale Mode: 选第2项,自适应屏幕宽。
Reference Resolution:参考分辨率,应根据应用情况而定,这里我们设成800×600,即画布的大小宽为800像素,高为600像素。
Screen Match Mode:屏幕自适应模式, 选第1项,自适应宽或高。
3、创建背景
在画布上创建一个Image控件,命名为Background,并为其赋值一个Sprits贴图作为背景图:
 
此图看起来会是朦胧的,究其原因是因其默认为半透明状,将其设为不透明,即可解决。方法为单击: ,会弹出:
       
其中的R、G、B表示红绿蓝颜色,最后一项A代表的就是透明度,将其滑块拖到最右,如上图右,即设成了不透明,返回后其效果如下图:
 
4、创建供滑动用的系列面板
再在画布上建立一个Image父控件,命名为Sliderpanel,并在其下建立5个Panel,其结构与命名如下:
 
调整这5个panel,使它们向右平铺开来,如下图。我们这样做的目的是想创建一个滑动效果,就像手机上的滑屏效果。
 
这5个面板默认是重叠在一起的,要像上图那样把它们向右平铺开来,其位置的调整,除用鼠标手工拖动外,其实有更精确的方法。
第1个面板MainPanel在原位,是不需调整的。
第2个面板Panel1应紧排在第1个面板的右边,我们选中Panel1,在其Inspector中的Rect Transform中设置Left为800,Right为-800,即可将Panel1精确地调整到MainPanel的右边第一个位置上。这里的Left、Right的值800、-800怎样理解?
我们以下列三个系列图来说明这个问题:
下图中的第1个,我们是在画布Canvas上创建一个面板Panel,并调整它的大小,使这个Panel的Left、Top、Right、Bottom分别为200、100、220、120,从图示就可看出它们分别表示子控件Panel的边与父控件Canvas相对应边之间的距离,这个距离的单位为像素。如果画布设置的参考分辨率为800×600,即宽800像素,高600像素,那么大家就可从子控件与父控件各边之间的距离(即空白距离)算出子控件的大小了。
          
             图1                                   图2

图3
图2与图3均是在Canvas的子控件Panel上创建的一个子子控件Panel,不过图2中的Canvas上的子控件Panel与Canvas一样大,图3中的Canvas上的子控件Panel比Canvas小,各边留空100,它们的子子控件Panel的Left、Top、Right、Bottom都是150像素,然而因父控件的大小不同,它们的大小也就不同了。从这个例子可看出,Lest、Top、Right、Bottom指的是本控件的边与父控件相对应的边之间的空白距离。
上例情况应该是好理解的。然而,这些值为负值时,又该怎样理解呢?
我们还是先从正值理解起,Left为150,表示子控件的左边缘在父控件左边缘的向右方向150像素处。同理,Right为150,表示子控件的右边缘在父控件右边缘的向左方向150像素处。反之,Right为-150,则表示子控件的右边缘在父控件右边缘的向右方向150像素处,即负表示反方向。
根据上面所讲原理,继续设置游戏界面,那么Panel2、Panel3、Panel4的Left分别为1600、2400、3200,Right分别为-1600、-2400、-3200,这样那5个面板就向右边并排排列开来了。(这里展示的是加上贴图后的效果,其贴图大家可自行完成)
 
根据前面的创建,从结构 上可看出有两个图像控件backguound与Sliderpanel,Sliderpanel上有5个Panel控件,backguound上有贴图,Sliderpanel作为父控件以便对其中的5个Panel统一移动、旋转等处理,其本身是没有贴图的,而那5个Panel中是有贴图的,总共有6个贴图存在,那么在其Scene或Game视图中,也许我们认为应该看到6个画面,而实际上在2D的Scene视图中看到的是: 在Game视图中看到的只有MainPanel的画面: 
如果我们单击Scene视图上方 中的 ,即可观察到3D模式下的状态:
 
这里好像也只有5个,没有6个,实际上这是因background与MainPanel在位置上是重叠的,而MainPanel又在background的前面,所以就看不到background了。
    选中background,将其Pos Z的值由0设为600,如下图:
 
此设置的结果是将background沿Z轴正方向即向前推600个像素,那么在3D视图中为:
 
    从这点上可看出,看不见,并不代表它不存在。
这时虽然在3D模式下的Scene视图中能观察到,但因Canvas的渲染模式为Screen Space – Camera,在Game视图中又由于MainPanel的遮挡,其background仍不见,但如果以后实现了滑动功能,MainPanel被滑开后就可见了,这样做的好处至少不会因MainPanel的滑开而露出一个空白底子。
这充分体现了Screen Space – Camera渲染模式下的3D设计,2D显示的功能。
Screen Space – Camera这是我们目前最常用的渲染模式。虽然我们可以放置一些3D模型,但显示总是2D的,你是不能看到它的侧面的。既然是这样,那么摄像机就用不着再使用透视Perspective模式了,可将其设置为Orthographic(正交) ,即选择第2项 ,其Scene视图的表现将由:
 
变为:
 
即视线为2D正交状,更有利于2D表现。
5、设计主界面上的元素
 
1个Text,显示文字“英雄救美”,采用富文本:英雄<color="red"><size="50">救</size></color>美;
3个按钮,左下角喇叭贴图的按钮用于调节音量;中间水晶球贴图的按钮用于开始游戏;右上角钱币贴图的按钮用于查看金币数量;
2个3D模型,1个美女3D模型,1个小汽车模型。
3D模型导入后,可能出现两种情况,一个是运行游戏时不可见;二个是其贴图丢失,造成白模。
第一种情况的原因是,当我们查看渲染的摄像机的Culling Mask属性时将发现其值为UI,即只有UI层才被渲染,故我们应将该模型的Layer设为UI 。那么为什么我们所添加的象Button、Text…等UI控件不需设置此步就可见呢,我们单击选中或任意创建一个UI控件,再查看其Layer将发现它们会被自动设置为UI,如下图:
 所以就不需我们再次去设定了。而3D模型添加到场景后,其Layer默认为Default,如果不手工改成UI就不能显示出来了。
第二种情况的解决办法,我们可以先将模型所用的材质贴图先复制到该工程项目中的一个文件夹内,并在3DMAX中将其对应的材质贴图的路径也改成该工程项目的那个文件夹中对应的材质贴图文件,然后把该模型直接导出为FBX格式到该工程项目中的一个模型文件夹中,这样它会自动对应到相应的材质贴图,再在场景中使用这个模型时就不会出白模情况了。
另外,当我们单击选中已导入Project中的3D模型时,在其Inspector视图中,选中Model选项卡,将发现Scale Factor的值为0.01,如下图所示:
 
3D模型导入Unity时默认是缩小100倍的,在3DMAX中,其单位往往默认是英尺英寸,而Unity是米,如果我们把MAX的单位设成米,并把“系统单位设置”里的系统单位比例也设成米,如下图:  
这样制作出来的模型导入到Unity中时,就可将Scale Factor设成1,即1:1导入,就不会出现大小不合适的情况了。
6、实现面板滑动
我们先给出滑动用的脚本:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;                           //UI命名空间
using UnityEngine.EventSystems;                //事件系统命名空间

public class MovingPanel : MonoBehaviour,IBeginDragHandler,IDragHandler {
    private float StartPosx;
    private float PosX;
void Start () {
}
    public void OnBeginDrag(PointerEventData eventData) {
        //print("OnBeginDrag " + eventData.position.x);
        StartPosx = eventData.position.x;
    }
    public void OnDrag(PointerEventData eventData)
    {
        //print("OnDrag " + eventData.position.x);
        PosX = eventData.position.x - StartPosx;
        this.transform.Translate(new Vector3(PosX/100,0,0),Space.World);
    }
void Update () {
}
}
IBeginDragHandler与IDragHandler是两个关于拖拽的接口类。IBeginDragHandler是处理开始拖拽的事件接口,IDragHandler是处理拖拽中的事件接口。实现接口,就必须实现接口中的所有方法,而这两个接口中有什么方法呢?如果脚本编辑器我们使用的是Visual Studio的话,在接口类名上单击鼠标右键,选择“转到定义”如下图:
 
就可查看到该接口中的方法:
#region 程序集 UnityEngine.UI.dll, v1.0.0.0
// C:/Program Files (x86)/Unity/Editor/Data/UnityExtensions/Unity/GUISystem/4.6.3/UnityEngine.UI.dll
#endregion

using System;

namespace UnityEngine.EventSystems
{
    public interface IBeginDragHandler : IEventSystemHandler
    {
        void OnBeginDrag(PointerEventData eventData);
    }
}
我们把其中的方法OnBeginDrag复制到我们的脚本中,不过复制过来后一定要在方法前加上Public关键字,因接口中的方法都是Public的,且是默认,所以在接口中就没有标示出Public的,如果我们不加上Public的话编辑器就会报错。
IDragHandler中的OnDrag方法,也采用同样的办法来处理。
我们可以先在这两个方法中只加上print语句,并运行程序,观察结果将发现,OnBeginDrag只运行一次,是在开始拖拽时。OnDrag运行多次,发生在拖拽中。明白此道理后,我们把print语句注释起来,加上控件拖动的语句:
StartPosx = eventData.position.x;是记录下刚开始拖拽时鼠标(对于移动设备是手指)的位置,PosX = eventData.position.x - StartPosx;是拖拽中的移动量,this.transform.Translate(new Vector3 PosX/100,0,0), Space.World);此句就是使面板在世界坐标中沿X轴向上移动PosX/100,为什么要除以100呢?因PosX是像素,而Translate的移动量的单位是米,除以100就相当于转化一下单位,否则本来是100像素就是移动100米了,会非常的快,快得不符常理了。当然这个100是通过试验方法来设定的,不是固定的。
把此脚本挂接到Sliderpanel上,因它是父控件,移动它就可移动那5个面板了。
7、定义滑动边界
虽然我们实现了滑动,但有一个问题,如下图状,在此基础上继续把面板向右滑动,且滑出主界面时,就不能再把鼠标放置到面板身上去把它滑回来,主界面上就只剩下那个背景图控件了,无法完成后续的操作。
 
下面我们先来观察一下面板的移动值,我们在脚本的Update中加上一句:print("x="+this.transform.position.x);运行后,当面板向右将要移出(如下图)时
 
显示的位置值如下图:
 
其值为由0到13.4,移动的这13.4是像素还是米?好象都不是。我们将此句改写成:print("x=" + this.transform.localPosition.x);,再运行,移动后发现,在面板向右将要移出时,打印出来的值为600多或700多,而我们的画布是800像素宽,说明此值是像素值。localPosition是局部坐标,是Sliderpanel相对它的父控件Canvas的坐标,是相对值,也正是我们想要的值。
下面我们就可利用此值来控制面板不被移出界面。
在Update中去掉刚才的Print语句,加入下列语句:
        if (this.transform.localPosition.x > 0)
        {
            this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(0, 0, 0), Time.deltaTime*10);
    }
滑动面板中的面板除第一张放在主界面上外,其他面板都是放在右边的,所以一开始应从右向左滑,后边的面板就会进入主界面中,如果从左向右滑,整个面板就会滑出界面。开始时,Sliderpanel的坐标为0,0,0,如果向左滑,其坐标值将减小,为负值,如果向右滑,其坐标值将增大,为正值。根据我们的设计,这个坐标值为正值时,面板就有可能滑出界面,所以if (this.transform.localPosition.x > 0)这句就是监测面板的坐标值,当我们向右滑时,其坐标值立马就会大于0,于是条件成立,下边的语句就会执行:
this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(0, 0, 0), Time.deltaTime*10);
这个语句的功能是设置挂接了此脚本的Sliderpanel控件的局部坐标值为新的坐标值,这个新坐标值是由Vector3中的一个函数Lerp计算出来的,它表示两个向量之间的线性插值,其函数原型是:static function Lerp (from : Vector3, to : Vector3, t : float) : Vector3,其功能是按照数字t在from到to之间插值,t是夹在 [0...1]之间,当t = 0时,返回from,当t = 1时,返回to。当t = 0.5 返回from和to的平均数。
以上解释是帮助文档中的内容,在其中还一个例子程序:
//像弹簧一样跟随目标物体
using UnityEngine;
using System.Collections;
public class example : MonoBehaviour {
public Transform target;
public float smooth = 5.0F;
void Update() {
transform.position = Vector3.Lerp(transform.position, target.position, Time.deltaTime * smooth);
}
}
这个例子是,当我们移动目标物体target时,另一个也就是挂接了此脚本的物体会像弹簧一样跟随目标物体,为什么会像弹簧一样呢?因Lerp会根据Time.deltaTime * smooth的值,在transform.position到target.position之间插值,并返回一个Vector3赋给挂接了此脚本的物体,此物体的坐标值由于有了新的值,而且这个值是在它们两者之间(插值),看起来这个物体就是向目标物体靠近,如果我们移动得快,它们两者之间的距离就越大,那个物体向目标物体靠近得也就越多,看起来就快些,反之就慢些,但随着时间的推移,Time.deltaTime * smooth的值最终会等于1,那么此时Lerp返回的值就是目标物体的坐标值,那么这个物体就会靠在目标物体上,也就是说那个物体始终不会脱离目标物体,其跟随快慢是随着目标物体移动的快而快,慢而慢,最终跟上,看起来就像在它们两个身上连接了一个弹簧一样。根据此,我们把static function Lerp (from : Vector3, to : Vector3, t : float) : Vector3中的参数t : float称为弹性系数,用此控制跟随的速度,在实际运用中,往往用Time.deltaTime来代替,这样物体的移动随着时间的消失,看起来显得更加平滑,再给它乘上一个浮点数smooth可更加灵活地控制这个“弹簧的弹性”了。
在我们这个脚本里的Vector3.Lerp(this.transform.localPosition, new Vector3(0, 0, 0), Time.deltaTime*10)语句中,this.transform.localPosition是挂接上此脚本的控件Sliderpanel在滑动过程中的实时坐标值,第二个参数是一个固定的new Vector3(0, 0, 0),这样一来,当Sliderpanel向右滑,坐标值大于0时,Sliderpanel会向Vector3(0, 0, 0)点返回靠拢,最终回到这点上来,保证了此面板不会滑出界外了。
以上是控制面板不会向右滑出界外,下面可以根据此原理设计出控制面板不会向左滑出界外。其代码如下:
        if (this.transform.localPosition.x <-3200)
        {
            this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-3200, 0, 0), Time.deltaTime * 10);
        }
为什么这里是-3200,是因总面板有5个,在界外的有4个,每个面板与画布一样大,其宽为800像素,4个800当然就是3200了。到此,脚本的全部代码如下:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;                           //UI命名空间
using UnityEngine.EventSystems;                //事件系统命名空间

public class MovingPanel : MonoBehaviour,IBeginDragHandler,IDragHandler {
    private float StartPosx;
    private float PosX;
// Use this for initialization
void Start () {

}
    public void OnBeginDrag(PointerEventData eventData) {
        //print("OnBeginDrag " + eventData.position.x);
        StartPosx = eventData.position.x;
    }
    public void OnDrag(PointerEventData eventData)
    {
        //print("OnDrag " + eventData.position.x);
        PosX = eventData.position.x - StartPosx;
        this.transform.Translate(new Vector3(PosX/100,0,0),Space.World);
    }
// Update is called once per frame
void Update () {
   //print("x="+this.transform.position.x);
        //print("x=" + this.transform.localPosition.x);
        if (this.transform.localPosition.x > 0)
        {
            this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(0, 0, 0), Time.deltaTime * 10);
        }
        if (this.transform.localPosition.x <-3200)
        {
            this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-3200, 0, 0), Time.deltaTime * 10);
        }
}
}
8、整板卡位
以上虽然保证了整个面板不会被滑出界外,但在滑动过程中,这5张面板交替出现在主界面上时,有很不好控制的现象的出现,如想要的面板可能一滑而过,即使滑到了想看到的面板,它可能一半在界面上,另一半在界外,如果往回滑一点时,可能是原来在界外的那一半在界面上了,而原来在界面上的那一半却又跑到另一侧的界外了,如果能让面板自动整板卡位到界面上,就会让人感觉到很舒适了。实际上,我们应用上面定义滑动边界的办法也能做到这点。
在Update中增加下列代码:
        //整版卡位
        if (this.transform.localPosition.x < 0 && this.transform.localPosition.x >= -800)
        {
            this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-800, 0, 0), Time.deltaTime * 5);
        }else if (this.transform.localPosition.x < -800 && this.transform.localPosition.x >= -1600)
        {
            this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-1600, 0, 0), Time.deltaTime * 5);
        }
        else if (this.transform.localPosition.x < -1600 && this.transform.localPosition.x >= -2400)
        {
            this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-2400, 0, 0), Time.deltaTime * 5);
        }
        else if (this.transform.localPosition.x < -2400 && this.transform.localPosition.x >= -3200)
        {
            this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-3200, 0, 0), Time.deltaTime * 5);
        }
该代码的工作原理,大家可根据前定义滑动边界的原理去分析它,应该是一样的。这里的弹性系数乘了一个5,而不是10,是觉得10时太快了,这个值大家可根据具体情况去调节。实际上,直接乘以一个值这个编程方法并不好,应该是设定一个变量,通过变量可方便调试,请大家自己优化。
运行程序将发现,向左滑动时很方便,很容易就卡位到我们想要的版面上了,但向右滑回时,就感觉不好了,很难卡位到想要的版面上了。分析一下我们的代码就不难发现其中的原因了。
开始时,Sliderpanel的坐标为(0,0,0),只要我们微微地向左滑动一点,Sliderpanel的X坐标就会小于0且大于-800,满足第一个条件if (this.transform.localPosition.x < 0 && this.transform.localPosition.x >= -800),在Lerp的作用下,程序会自动将Sliderpanel坐标定位在(-800,0,0)上,也就是自动卡位到第2张面板上。同理,在此基础上,再向左滑动,只要你稍稍一动,Sliderpanel的X坐标就会小于-800且大于-1600,满足第二个条件if (this.transform.localPosition.x < -800 && this.transform.localPosition.x >= -1600),在Lerp的作用下,程序会自动将Sliderpanel坐标定位在(-1600,0,0)上,也就是自动卡位到第3张面板上。但此时如果我们向右滑回,即想滑回第2张面板上去,但只要你滑动得不是很大的范围,其Sliderpanel的X坐标仍会小于-800且大于-1600,仍然满足第二个条件,那么Sliderpanel仍然卡位到第3张面板上,不会按照我们的意愿卡位到第2张面板上去,这就是向左滑时很智能,向右滑时很犟蹙的原因。
那么,我们怎样让程序知道用户滑动的方向呢?程序中正好有一个private float PosX这个量,它在OnDrad方法中赋值PosX = eventData.position.x – StartPosx,当它为负时就可表示向左滑,为正时就表示向右滑,于是我们在卡位程序中的每个判断语句中嵌套一个判断语句,让程序根据用户的滑动方向来卡位,就会智能化了。如第一个判断语句中嵌套判断语句后是:
if (this.transform.localPosition.x < 0 && this.transform.localPosition.x >= -800)
{
     if (PosX < 0)
     {
         this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-800, 0, 0), Time.deltaTime * 5);
     }else{
         this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(0, 0, 0), Time.deltaTime * 5);
     }
}
PosX小于0,就是向左滑,卡位到第2张,否则大于0,说明是向右滑,就卡位到第1张。余下的判断语句依此改写,那么面板的滑动就完善了,至此,整个脚本的内容如下:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;                           //UI命名空间
using UnityEngine.EventSystems;                //事件系统命名空间

public class MovingPanel : MonoBehaviour,IBeginDragHandler,IDragHandler {
    private float StartPosx;
    private float PosX;
// Use this for initialization
void Start () {
}
    public void OnBeginDrag(PointerEventData eventData) {
        //print("OnBeginDrag " + eventData.position.x);
        StartPosx = eventData.position.x;
    }
    public void OnDrag(PointerEventData eventData)
    {
        //print("OnDrag " + eventData.position.x);
        PosX = eventData.position.x - StartPosx;
        this.transform.Translate(new Vector3(PosX/100,0,0),Space.World);
    }
// Update is called once per frame
void Update () {
   //print("x="+this.transform.position.x);
        //print("x=" + this.transform.localPosition.x);
        //移动范围受限处理:右边沿
        if (this.transform.localPosition.x > 0)
        {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(0, 0, 0), Time.deltaTime * 10);
        }
        //移动范围受限处理:左边沿
        if (this.transform.localPosition.x <-3200)
        {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-3200, 0, 0), Time.deltaTime * 10);
        }
        //整版卡位
        if (this.transform.localPosition.x < 0 && this.transform.localPosition.x >= -800)
        {
            if (PosX < 0)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-800, 0, 0), Time.deltaTime * 5);
            }else{
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(0, 0, 0), Time.deltaTime * 5);
            }
        }else if (this.transform.localPosition.x < -800 && this.transform.localPosition.x >= -1600)
        {
            if (PosX < 0)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-1600, 0, 0), Time.deltaTime * 5);
            }
            else {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-800, 0, 0), Time.deltaTime * 5);
            }
        }
        else if (this.transform.localPosition.x < -1600 && this.transform.localPosition.x >= -2400)
        {
            if (PosX < 0)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-2400, 0, 0), Time.deltaTime * 5);
            }
            else
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-1600, 0, 0), Time.deltaTime * 5);
            }
        }
        else if (this.transform.localPosition.x < -2400 && this.transform.localPosition.x >= -3200)
        {
            if (PosX < 0)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-3200, 0, 0), Time.deltaTime * 5);
            }
            else
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-2400, 0, 0), Time.deltaTime * 5);
            }
        }
}
}

实际上以上代码还可继续优化,优化后的代码如下:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;                           //UI命名空间
using UnityEngine.EventSystems;                //事件系统命名空间

public class MovingPanel : MonoBehaviour,IBeginDragHandler,IDragHandler {
    private float StartPosx;
    private float PosX;                                             //X轴向上移动的距离
// Use this for initialization
void Start () {
}
    public void OnBeginDrag(PointerEventData eventData) {           //开始拖
        //print("OnBeginDrag " + eventData.position.x);
        StartPosx = eventData.position.x;
    }
    public void OnDrag(PointerEventData eventData)                  //拖动过程中
    {
        //print("OnDrag " + eventData.position.x);
        PosX = eventData.position.x - StartPosx;
        this.transform.Translate(new Vector3(PosX/100,0,0),Space.World);
    }
// Update is called once per frame
    void Update()
    {
        //print("x="+this.transform.position.x);
        //print("x=" + this.transform.localPosition.x);
        //整版卡位
        if (PosX < 0)
        {
            if (this.transform.localPosition.x < 0 && this.transform.localPosition.x >= -800)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-800, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x < -800 && this.transform.localPosition.x >= -1600)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-1600, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x < -1600 && this.transform.localPosition.x >= -2400)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-2400, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x < -2400 && this.transform.localPosition.x >= -3200)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-3200, 0, 0), Time.deltaTime * 5);
            }
            else
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-3200, 0, 0), Time.deltaTime * 5);
            }
        }
        else
        {
            if (this.transform.localPosition.x > -3200 && this.transform.localPosition.x <= -2400)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-2400, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x > -2400 && this.transform.localPosition.x <= -1600)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-1600, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x > -1600 && this.transform.localPosition.x <= -800)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-800, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x >= -800 && this.transform.localPosition.x <0)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(0, 0, 0), Time.deltaTime * 5);
            }
            else
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(0, 0, 0), Time.deltaTime * 5);
            }
        }
    }
}

9、3D模型旋转动画
下面我们做出小汽车不停旋转的动画。
在Hierarchy视图中选中小汽车模型,然后单击系统菜单Window中的Animation,如下图:
 
此时会弹出Animation窗口:
 
单击红色的录制按钮 ,会弹出:
 
选择并打开保存将要录制的动画文件的文件夹_Animator,并为此动画命名为car_Rotation,保存后,动画录制界面如下图所示:
 
在Animation的帧数框中输入60,在Inspector中的Rotation的Y框中输入60,如下图:
 
其意思为,到60帧时,围绕Y轴旋转60度。
在Animation的帧数框中输入120,在Inspector中的Rotation的Y框中输入120,如下图:
 
其意思为,到120帧时,围绕Y轴旋转120度。
在Animation的帧数框中输入180,在Inspector中的Rotation的Y框中输入180,如下图:
 
其意思为,到180帧时,围绕Y轴旋转180度。
在Animation的帧数框中输入240,在Inspector中的Rotation的Y框中输入240,如下图:
 
其意思为,到240帧时,围绕Y轴旋转240度。
在Animation的帧数框中输入300,在Inspector中的Rotation的Y框中输入300,如下图:
 
其意思为,到300帧时,围绕Y轴旋转300度。
在Animation的帧数框中输入360,在Inspector中的Rotation的Y框中输入360,如下图:
 
其意思为,到360帧时,围绕Y轴旋转360度。
设置完后,单击红色录制按钮停止录制,并保存一下场景,我们将发现:
1、 在Project中展开_Animator文件夹,其中将出现两个与刚才录制的动画有关的文件,如下图: 
2、 在Inspetor视图中,会自动为该模型增加一个Animator组件,如下图:
 其Controller也会自动赋值。
此时我们运行程序,将发现那个小汽车会不停地旋转起来,即录制的动画已成功加载,当我们在Project中选中car_Rotation那个动画文件时,其Inspetor视图中的Loop Time是勾选的,所以,动画会自动播放,如下图:

10、声音设置面板移动动画
象前面第十三节:一个简单的游戏主界面一样,在这里我们建立一个设置声音的面板,当单击主界面上的声音设置按钮后,从上往下滑出声音设置面板,设置完后单击声音设置面板里的确定按钮,声音设置面板从下往上滑回。
首先建立这个声音面板,在Canvas上单击鼠标右键,创建一个UI中的Panel,命名为SetAudio,并设置它的Source Image,给它一个图片精灵,如下图:
 
该面板遮挡了主界面上UI元素,却没有遮挡住3D元素,把视图切换到3D形式,可看出该面板与Slederpanel重合,美女与车两个3D模型在它的前边,所以不能遮挡它们。
 
我们将其Z坐标值调小到如-500,比美女与车的Z值都还要小,即可遮挡它们了。
 
我们的目的是单击主界面上的声音设置按钮后,该面板从上往下滑出,遮挡所有,它成为主界面,设置完后,单击声音设置面板上的确定按钮后滑回。所以,该声音面板的初始位置应在该位置处的上边,所以继续把面板垂直住上调:
 
然后再在声音设置面板上创建一个Button,显示文字为确定,用于设置完后的返回。
这些准备工作做好后,下边着手创建该面板的动画。选中该声音设置面板,单击系统菜单Window中的Aniation
 
弹出Animation:
 
单击 中的红色录制按钮,会弹出:
 
此时要求我们给录制的动画取名与选择保存此动画的路径,我们假设为此动画取名为ShowSetAudio,保存在_Animator中:
 
单击保存后:
 
以前那个红色按钮是这种状态: ,现在是: ,有点泛红晕状,表示现在处于录制状态。这个 中最后一个为0的数字框即为帧数框,现在处于0帧位置处,我们把0改成60,即60帧,告诉动画机,下边我们录制第60帧处的动画状态,此时录制面板中的那条红色竖线将移动到1:00处(60帧大约为1秒),如下图:
 
根据动画设计要求,此时面板应该是滑下来,所以在Sceene中我们把面板沿Y轴向下拖到,动画录制机会实时记录下面板的相对位置,为了准确,我们可手工在处于暗红色的 Top与Bottom框中输入值0,即此时声音设置面板的上下边离它父控件的上下边均为0像素,完全滑下来。其设置窗口界面如下图:
 
我们还可发现,在现在位置的红线处会出现关键帧图标: 。
从底部的 处可见,现在的视图是Dope Sheet(关键帧清单),当我们点击SetAudio:Anchored Position.y并点击Curves(曲线)时,会转换为曲线视图,如下图
 
如果曲线过大显示不全或过小看不清时,可调节鼠标中键滑轮,缩放曲线到适当大小。
曲线上的绿色点为关键帧点,可看出此动画只有两个关键帧,用鼠标单击关键帧时,Scene视图中的面板就会移动到所设置的位置上。
此时,我们点击SetAudio:Size Delta.y,也可观察此项的曲线,只不过该项值没有变化,为一平直的直线:
 
设置好后,单击红色的录制按钮一下,即可停止录制且保存动画。
这是显示声音设置面板的动画,下面我们来设计隐藏声音设置面板的动画。
单击已经显示出刚才录制的动画名为ShowSetAudio这个框右边的按钮,弹出:
 
单击[Create New Clip],创建一个新的动画剪辑,此时会同样弹出一个要求你为新动画取名以及选择保存路径的对话窗,假设我们为此动画取名为HideSetAudio.anim,路径与前一个动画相同,其后的动画设计与前边一样,只不过第0帧时,面板在下,第60帧时,面板在上。
动画创建结束后,即可直接关闭录制窗体,系统会自动保存动画文件。回到系统界面下,在Project视图中的我们事先设定的保存动画的_Animator文件夹下将会新增三个文件: ,而且在Hierarchy视图中选中已设置了动画的控件SetAudio,在其Inspector视图中将发现系统会为其自动增加动画组件Animator,而且其中的Controller并已自动赋值为已录制了动画的控件SetAudio:
 。
好了,万事具备,运行程序,但发现那个面板的动画会不断重复播放,其原因是其Loop Time的属性值为True,如果设为False即可去掉其默认的自动播放特性。方法为,在Project视图中选中 动画,然后在其Inspector视图中,把 后的钩去掉即可(同理,其HideSetAudio动画也做这样的处理),但程序运行时,面板虽不会重复播放显示动画了,但它会播放一次,这与我们的设计初衷不符,我们的初衷是程序运行时不显示面板,当我们点击设置按钮后才会显示面板,点击面板中的确定按钮后,隐藏面板。要达到这种功能,就要设置动画状态机中的一些属性以及配合使用脚本才能完成。
首先,让动画不自动播放。
在Hierarchy视图中选中设置了动画的控件SetAudio,可发现其Inspector视图中Animator组件中的第一个属性Controller(控制器,也称动画状态机)的值为前边录制的 动画:SetAudio,如图: ,单击其后的设置按钮 可更改为其他动画,不过这里是不需更改的,我们双击动画框中的已有的动画SetAudio,会打开Animator窗口:
 
其中有两个按钮代表的就是按钮文字所指示的动画剪辑:ShowSetAudio、hideSetAudio,即现在这个SetAudio动画里有两个动画剪辑。我们在面板的空白地方单击鼠标右键: ,选择第一项Create State中的Empty,创建一个空动画,其默认名为New State,如图:
 
在New State上点右键,在弹出的 中选第二项Set As Default,即设置这个空动画为默认播放的动画,既然为空动画,自然就不会播放动画,经此设置后的窗口如下图所示:
 
比较设置前后两图可发现,代表默认动画的按钮的颜色为黄红色。即哪个按钮为黄红色,那个动画剪辑就是当前动画播放时的状态位置,既然当然动画为空动画,自然就不会播放动画,动画状态机的命名来源也许就是这个原因吧。
经此设置后,再次运行程序,就不会有动画播放了。
通常情况下,一个动画里有许多的动画剪辑,到底播放哪个动画,是动画状态机来决定的。 这个界面就是动画状态机界面,它表示现在播放的动画是它中的那个红黄色所表示的动画剪辑。那么,我们有什么办法来控制动画状态,或者说让动画在不同的剪辑中交替转换呢?
我们在“New State”上单击鼠标右键,选择第一项Make Transition(设置转换),然后直接移动鼠标,将会出现一个带前头的连线,再在”ShowSetAutio”上单击鼠标左键,即可在这两个动画剪辑上建立转换联接: ,这个连线表示,当满足某个条件时,就可由播放剪辑New State变成播放剪辑ShowSetAudio,即使ShowSetAudio处于播放状态。这个条件又在哪里设置呢?单击 右边的“+”按钮,在弹出的 中选Bool,即设置一个布尔型参数,并把此参数假设命名为IsDisplay,如图 。
设置好参数后,用鼠标单击那根连接线,它将变成蓝色,表示选中,如图:
 
接下来,在其Inspector视图中的Conditions设置条件变量为刚才建立的IsDisplay,条件值为true,如下图:
 
其意思为,当IsDiaplay为真true时,动画播放状态由New State转变为ShowSetAudio。
同理,可设置由hideSetAudio到ShowSetAudio转变条件也为当IsDisplay为真true时。
 
这两步设置的结果我们可以这样理解,当IsDiaplay为真true时,动画播放状态处于New State或hideSetAudio时,都将转到去播放ShowSetAudio。
依此类推,当IsDiaplay为假false时,动画播放状态如果处于ShowSetAudio时,将转到去播放hideSetAudio。这样其动画状态我们就设定完了,如下图:
 
接下来我们利用脚本去改变那个变量的值,就可实时控制动画的播放了。
脚本内容如下(红色部分为增加的脚本内容):
using UnityEngine;
using System.Collections;
using UnityEngine.UI;                           //UI命名空间
using UnityEngine.EventSystems;                //事件系统命名空间
public class MovePanelControl: MonoBehaviour {
    public Animator Ani_SetAudio;
// Use this for initialization
void Start () {

}
public void showSetAudio(){
  Ani_SetAudio.SetBool("IsDisplay",true);
}
public void HideSetAudion(){
    Ani_SetAudio.SetBool("IsDisplay", false);
}
// Update is called once per frame
void Update () {
}
}
在场景中创建一个空对象,命名为_ MovePanelControl,专门用来控件面板的移动,把脚本挂在它身上,并在Inspector视图中把Hierarchy中的SetAudio赋给Ani_SetAudio,如图: 。
脚本中Ani_SetAudio变量是Animator类型,为何可以把控件SetAudio赋值给它呢?道理是SetAudio控件已经有了Animator组件,程序会自动抽取其中的Animator出来赋值给变量Ani_SetAudio。
Ani_SetAudio.SetBool ("IsDisplay",true);其中的IsDisplay就是在状态机中增设的参数,其拼写必须与状态机中的完全一致,否则不能运行。
最后设定“设置”与“确定”两个按钮的委托事件。
在Button组件的下方有一个OnClick()选项,这就是Button控件处理事件的重要机制。
OnClick()意思为当该按钮被点击时所发生的事件:
 
此时其事件列表为空:List is Empty,我们单击其下的“+”按钮为其添加一个事件:
 
此时事件虽被添加了,但其委托的事件处理对象为空:None(Object),当然连事件处理对象都没有,其事件处理方法自然也就为空:No Function。那么怎样委托事件处理对象与选择事件处理方法呢?
我们把层级面板中刚才建好的并已挂上了事件处理脚本的那个专门用来处理面板移动的空对象_ MovePanelControl拖放到None(Object)框中即可,此时此框中显示的内容即为委托的此事件处理对象的名称_MovePanelControl (MovePanelControl): ,括号中的MovePanelControl表示此对象是挂上了名为MovePanelControl的脚本。有了事件处理对象了,然后使用该对象的什么方法来处理事件呢?这还需我们给它指定,其方法是单击显示内容为No Function的那个事件方法框,会弹出菜单列表:
 
当我们的鼠标指向我们自己编写的脚本MoveSetAudio时会继续展开,其中就有我们在脚本中编写的事件处理方法:showSetAudio(),选中它即可,设置后: 这样就完成了事件的委托,当我们运行时,单击那个那个声音设置按钮,其声音设置面板就会从上往下滑出。
其声音设置面板的隐藏按钮事件的设置方法同上。
以上的设计原理,用一个图大致表示如下:

11、UI面板旋转动画
 
我们在Sliderpanel的子控件Panel4上创建一个像风扇一样的物体,并让它旋转。其组织结构如下图:
 
Panel是一个父控件,只要我们旋转这个父控件,其子控件也就会跟着旋转,也就达到了旋转风扇的视觉效果了。该Panel的透明度需调成完全透明,否则会影响子控件所构成的图案,如下图:
 
风扇一样的图案,是由4个有贴图的子控件拼成,中间的那一个是一个Button,目的是单击它时将进入游戏。其余3个均为同一贴图的Image控件,各自旋转不同的角度而构成扇叶状。
我们选中父控件Panel,单击 后进入Animation:
 
按照前面所讲的方法录制旋转动画,只不过前边我们多数是移动,现在是旋转,所以不同帧数时要改变Rotation的Z值,如下图:
 
录制完后保存即可,因其动画默认是循环播放,运行程序就可看到效果了。
12、枪支复杂动画
    设计思路如下,当程序运行后,如下图,这是起始界面:
 
我们向左滑动面板,进入第二、三、四等面板,分别展示不同的枪支。下在以第二个面板上的设计为便进行讲解:
 
该面板展示一支枪,标明价格为8888,点击 按钮会从上往下滑出一面板,如下图:
 
该面板会遮挡它后面元素,用于枪支展示,此时我们应做出动画来,让枪支从该面板的后面移动到该面板的前边来,并且逐渐变大,如下图:
 
当用户点击 按钮时,枪支变小并移回,同时该展示面板向上滑出,回到游戏界面:

首先,在Canvas中的Sliderpanel中的Panel1上创建下列控件:
 
Pal_Background是一个面板,颜色为绿色,做背景用。
Gun是一个3D枪支模型。
Btn_DisplayGuns是一个带向下箭头贴图的按钮 ,点击它时会有一个展示该枪支的面板从上往下滑出。
Image是一个Image控件 ,贴有钱币图。
Text是一个文件框 ,显示该枪支的售价。

展示面板 我们直接在Canvas中创建,并命名为WeaponDisplayPanel,其中有一个带向上箭头贴图的按钮,用于滑回此面板用:
 
该面板的空间位置在左右方向上不动即Left与Right均为0,竖直方向上要往上,使其游戏运行中不可见,在单击了那个 按钮后才滑出:
 
前后方向上即Z轴值要往前,以遮住主界面为准:

以上准备工作完备后,下部就是做动画了。
首先我们来做展示面板滑出、滑回的动画,我们在Hierarchy视图中选中该面板WeaponDisplayPanel,单击系统Window菜单,选择Animation,弹出Animation窗口,在此分别做出滑下WeaponPaneldown的动画:
 
滑回WeaponPanelUp的动画:
 
像前边所讲的面板滑动动画一样,进一步设置该动画的状态机,去掉自动播放功能:
 
至于脚本,我们前面总是单独建立,实际上在实际开发工作中,有些脚本是可归类在一个脚本文件中,这里我们在前面所建立的MovePanelControl面板滑动脚本文件中,增加控制这个武器展示面板的脚本,增加后的代码如下(红色部分为增加的代码):
using UnityEngine;
using System.Collections;
using UnityEngine.UI;                           //UI命名空间
using UnityEngine.EventSystems;                //事件系统命名空间

public class MovePanelControl : MonoBehaviour
{
    public Animator Ani_SetAudio;
    public Animator Ani_WeaponPanel;
// Use this for initialization
void Start () {

}
    public void showSetAudio()                      //显示声音设置面板
    {
Ani_SetAudio.SetBool("IsDisplay",true);
    }
    public void HideSetAudion()                      //隐藏声音设置面板
    {
Ani_SetAudio.SetBool("IsDisplay", false);
    }

public void showWeaponPanel()                   //显示武器展示面板
    {
        Ani_WeaponPanel.SetBool("IsDisplay", true);
    }
    public void HideWeaponPanel()                   //隐藏武器展示面板
    {
        Ani_WeaponPanel.SetBool("IsDisplay", false);
    }
// Update is called once per frame
void Update () {
        
}
}
同理,我们也用不着再单独创建事件处理对象,仍采用原来的空对象_MovePanelControl,只不过因在脚本中新增了变量public Animator Ani_WeaponPanel;需将实现动画的对象WeaponDisplayPanel拖放给该变量:
 
这些设置好后,再给显示面板与隐藏面板的那两个按钮分别委托事件处理即可完成该动画的制作:
 
下面制作枪支前移并放大、后移并缩小的动画。该动画为一系列的动画,是一枚举类型,故需Animation,不需要带动画状态机的Animator,而3D模型导进来时,里面默认的动画组件为Animator,故我们需先去掉该组件: 然后再给这个3D模型添加Animation组件: 
然后在Hierarchy中选中枪支模型gun,单击系统Window菜单中的Animttion,首先创建枪支前移动画gun_forwordMove:
 
接着创建后移动画gun_backMove:
  
再接着创建枪支变大动画:
 
 
最后接着创建枪支变小动画:
 
 
动画创建完之后,其Animation组件的属性如下图:
 
其中枚举长度为4: ,其枚举项 到 中的值就是我们刚才所创建那4个动画。
这4个动画播放哪一个,何时播放,就需脚本来控件了。我们打开MovePanelControl脚本,在其中增加要播放动画的脚本(增加的部分为红色):
using UnityEngine;
using System.Collections;
using UnityEngine.UI;                           //UI命名空间
using UnityEngine.EventSystems;                //事件系统命名空间

public class MovePanelControl : MonoBehaviour
{
    public Animator Ani_SetAudio;
    public Animator Ani_WeaponPanel;
    public Animation Ani_Gun;
// Use this for initialization
void Start () {

}
    public void showSetAudio()                      //显示声音设置面板
    {
Ani_SetAudio.SetBool("IsDisplay",true);
    }
    public void HideSetAudion()                      //隐藏声音设置面板
    {
Ani_SetAudio.SetBool("IsDisplay", false);
    }

public void showWeaponPanel()                   //显示武器展示面板
    {
        Ani_WeaponPanel.SetBool("IsDisplay", true);
        Ani_Gun.Play("gun_forwordMove");
        Ani_Gun.PlayQueued("gun_big");
    }
    public void HideWeaponPanel()                   //隐藏武器展示面板
    {
        Ani_Gun.Play("gun_backMove");
        Ani_Gun.PlayQueued("gun_small");
        Ani_WeaponPanel.SetBool("IsDisplay", false);
        
    }
// Update is called once per frame
void Update () {
        
}
}
    其Play()与PlayQueued()都是Animation类中两个方法,Play()为播放名称为括号中的String参数指定的动画,或者播放默认动画。PlayQueued()是播放队列,是在前一个动画播放完成之后播放下一个动画。
至此,枪支动画就算创建完成,当然我们自己还可再增加枪支动画,比如加入粒子系统,让枪试射等更加绚丽的动画。
13、音频管理开发
在前面第10节处,我们建立了设置声音的面板,在主界面上单击那个小喇叭按钮,会上往下滑出声音设置面板,该面板上除了一个确定按钮外,还没有音频管理用的控件。现在我们利用此面板来控制背景音乐与音效的音量大小,添加两个Text与两个Slider,如下图所示:
 
然后在场景中创建一个空对象(不是在Canvas上创建),命名为_AudioManger:
 
选中该对象,在Inspector视图中,单击Add Component按钮,为其添加两个组件:AudioListener与AudioSource:
 
一个为声音监听器,一个为声音源,一个相当于耳朵,一个相当于嘴,把它们同时加在一个对象上,这样我们就总是能听得见声音的(但也不一定,如上图中的Position的Z值为192.304,离我们很远,运行游戏时,将听不到声音的,故这里我们要将X、Y、Z的值重置为0,这样就总能听见声音的)。
在场景的创建时,系统会为我们的摄像机自动添加一个AudioListener,这里我们已经添加了AudioListener到这个空对象上了,就用着再在摄像机上添加AudioListener了,故需去除摄像机的AudioListener。

编写音频管理脚本:
/*
*音频管理器
 */
using UnityEngine;
using System.Collections;
using System.Collections.Generic;                                //泛型集合命名空间

public class AudioManager : MonoBehaviour {
    public AudioClip[] AudioClipArray;                           //音频文件数组
static private Dictionary<string,AudioClip> _Dic;     //音频文件集合
    static private AudioSource[] AudioSourceArray; //背景音乐数组
static private AudioSource _AudioSource_backgroundMusic;     //背景音乐音频源
static private AudioSource _AudioSource_AudioEffect;         //音效音频源
// Use this for initialization
void Awake(){
//音频源集合类的加载处理
_Dic = new Dictionary<string,AudioClip> ();
foreach (AudioClip audioClipItem in AudioClipArray) {
            _Dic.Add(audioClipItem.name, audioClipItem);
}
//得到Audiosource
AudioSourceArray = this.gameObject.GetComponents<AudioSource> ();
_AudioSource_backgroundMusic = AudioSourceArray [0];
_AudioSource_AudioEffect = AudioSourceArray [1];
}
//播放背景音乐
public static void PlayBackground(AudioClip audiClip){
//不能与当前正在播放的背景音乐重复
if (_AudioSource_backgroundMusic.clip == audiClip) {
return;
}
//得到GlobalManger传过来的音量大小
_AudioSource_backgroundMusic.volume = 1F;

//播放
if (audiClip) {
_AudioSource_backgroundMusic.clip = audiClip;        //注入音频文件
_AudioSource_backgroundMusic.Play ();
} else {
Debug.LogError("背景音乐文件不能为空");
}
}
//播放背景音乐  方法重载  根据背景音乐文件的名称来播放
public static void PlayBackground(string StrAudioClipName){
if (!string.IsNullOrEmpty (StrAudioClipName)) {
PlayBackground(_Dic[StrAudioClipName]);
}else {
Debug.LogError("背景音乐文件不能为空");
}
}
//播放音效
    public static void PlayEffect(AudioClip audiClip)
    {

//得到GlobalManger传过来的音量大小
        //_AudioSource_AudioEffect.volume = 1F;

//播放
if (audiClip) {
_AudioSource_AudioEffect.clip = audiClip;             //注入音效文件
_AudioSource_AudioEffect.Play ();
} else {
Debug.LogError("音效文件不能为空");
}
}
//播放音效音乐  方法重载  根据音效文件的名称来播放
    public static void PlayEffect(string StrAudioClipName)
    {
if (!string.IsNullOrEmpty (StrAudioClipName)) {
            PlayEffect(_Dic[StrAudioClipName]);
}else {
Debug.LogError("音效文件不能为空");
}
}
}
把此脚本挂到刚才创建的空对象_AudioManger上,其Inspector视图如下:
 
其中的 即为脚本中的public AudioClip[] AudioClipArray变量,从Size为0可看出其数组长度为0,在此我们设为2,如下图:
 
我们让第一个元素代表背景音乐,第二个代表音效,把对应的音频剪辑拖放到此处:
 
再为此对象添加一个AudioSource组件:
 
即让它有两个AudioSource组件,并使它们的AudioClip为空,去掉PlayOnAwake的对勾不自动播放:
 
为什么要给它两个AudioSource组件呢?请看下列代码:
AudioSourceArray = this.gameObject.GetComponents<AudioSource> ();
_AudioSource_backgroundMusic = AudioSourceArray [0];
_AudioSource_AudioEffect = AudioSourceArray [1];
因有背景音乐与音效音乐,至少需要两个,this.gameObject.GetComponents<AudioSource> ()即可获得这个对象上的那两个AudioSource组件,用AudioSourceArray [0]初始化背景音乐,用AudioSourceArray [1]初始化音效音乐。
这段代码是在Awake中执行的,而不是Start中执行,这两者有什么区别呢,简单说明一下,Awake在MonoBehavior创建后就立刻调用,Start将在MonoBehavior创建后在该帧Update之前,在该Monobehavior.enabled == true的情况下执行。
void Awake (){  
}       
//初始化函数,在游戏开始时系统自动调用。一般用来创建变量之类的东西。  
  
void Start(){  
}  
//初始化函数,在所有Awake函数运行完之后(一般是这样,但不一定),在所有Update函数前系统自动条用。一般用来给变量赋值。  
同时,在Awake中还有一段代码:
_Dic = new Dictionary<string,AudioClip> ();
foreach (AudioClip audioClipItem in AudioClipArray) {
            _Dic.Add(audioClipItem.name, audioClipItem);
}
首先创建音频文件集合_Dic = new Dictionary<string,AudioClip> (),然后通过forea语句把AudioClipArray 即 中的元素添加到音频文件集合中。
下边是列出了变量名、方法名,未列出方法体等简化的代码:
public class AudioManager : MonoBehaviour {
    public AudioClip[] AudioClipArray;                           //音频文件数组
static private Dictionary<string,AudioClip> _Dic;     //音频文件集合
    static private AudioSource[] AudioSourceArray; //背景音乐数组
static private AudioSource _AudioSource_backgroundMusic;     //背景音乐音频源
static private AudioSource _AudioSource_AudioEffect;         //音效音频源
void Awake(){ }
//播放背景音乐
public static void PlayBackground(AudioClip audiClip){ }
//播放背景音乐  方法重载  根据背景音乐文件的名称来播放
public static void PlayBackground(string StrAudioClipName){ }
//播放音效
  public static void PlayEffect(AudioClip audiClip){ }
//播放音效音乐  方法重载  根据音效文件的名称来播放
  public static void PlayEffect(string StrAudioClipName){ }
}
方法前边的static修饰符说明此方法为静态方法,静态方法的好处是不需实例化对象就可调用该方法,但静态方法里调用的变量必须为静态变量,因实例变量必须依附于实例对象,而静态方法是在未实例化对象时就调用了,所以不能调用实例变量,这就是那几个变量设为静态变量的原因。
音频管理器编写好后,在何处调用里面的方法来播放背景音乐或音效音乐呢?实际上这里我们不需再建立对象了,可以在原控制面板滑动的MovingPanel.cs脚本里调用,如要播放背景音乐,在Start中增加一句AudioManager.PlayBackground("BloodBag");即可达到目的。如想滑动面板时播放我们设置的音效,可在OnDrag中增加一句AudioManager.PlayEffect ("E5017");即可,这里不能在Update中增加该句,因每一帧都要播放一次,实际结果就是不能播放。
MovingPanel.cs脚本内容如下:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;                           //UI命名空间
using UnityEngine.EventSystems;                //事件系统命名空间

public class MovingPanel : MonoBehaviour,IBeginDragHandler,IDragHandler {
    private float StartPosx;
    private float PosX;                                             //X轴向上移动的距离
// Use this for initialization
void Start () {
//播放背景音乐
        AudioManager.PlayBackground("BloodBag");
}
    public void OnBeginDrag(PointerEventData eventData) {           //开始拖
        //print("OnBeginDrag " + eventData.position.x);
        StartPosx = eventData.position.x;
    }
    public void OnDrag(PointerEventData eventData)                  //拖动过程中
    {
        //print("OnDrag " + eventData.position.x);
        PosX = eventData.position.x - StartPosx;
        this.transform.Translate(new Vector3(PosX/100,0,0),Space.World);
        AudioManager.PlayEffect("E5017");
    }
// Update is called once per frame
    void Update()
    {
        //print("x="+this.transform.position.x);
        //print("x=" + this.transform.localPosition.x);
        //整版卡位
        if (PosX < 0)
        {
            if (this.transform.localPosition.x < 0 && this.transform.localPosition.x >= -800)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-800, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x < -800 && this.transform.localPosition.x >= -1600)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-1600, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x < -1600 && this.transform.localPosition.x >= -2400)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-2400, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x < -2400 && this.transform.localPosition.x >= -3200)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-3200, 0, 0), Time.deltaTime * 5);
            }
            else
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-3200, 0, 0), Time.deltaTime * 5);
            }
        }
        else
        {
            if (this.transform.localPosition.x > -3200 && this.transform.localPosition.x <= -2400)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-2400, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x > -2400 && this.transform.localPosition.x <= -1600)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-1600, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x > -1600 && this.transform.localPosition.x <= -800)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(-800, 0, 0), Time.deltaTime * 5);
            }
            else if (this.transform.localPosition.x >= -800 && this.transform.localPosition.x <0)
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(0, 0, 0), Time.deltaTime * 5);
            }
            else
            {
                this.transform.localPosition = Vector3.Lerp(this.transform.localPosition, new Vector3(0, 0, 0), Time.deltaTime * 5);
            }
        }
    }
}

如果想给按钮也加上音效,我们先写一个脚本:
using UnityEngine;
using System.Collections;

public class AudioPlanyControl : MonoBehaviour {

// Use this for initialization
void Start () {

}

// Update is called once per frame
    public void PlayButtonAudioEffect()
    {
        AudioManager.PlayEffect("E5017");
    }
}
在Canvas中创建一个空对象,命名为_AudioPlanyControl,并把此脚本做为它的组件,然后为要添加音效的按钮增加单击事件委托处理机制,当单击按钮时调用AudioManager.PlayEffect("E5017");播放指定的音效文件:
 
AudioManager.cs这个文件中的AudioManager类是一个音频管理基类,在这里加载音频源,得到Audiosource,初始化背景音乐与音效音乐,集成了播放背景音乐或音效音乐的方法,该类方法由于重载,可根据音频剪辑或音频剪辑的名称来进行播放。这个类是一个音频管理器,且其中的方法为静态方法,可供其他类来调用。比如,我们想左右滑动面板时播放音效,于是我们在滑动面板的MovingPanel.cs这个类中的OnDrag这个方法中调用AudioManager中的PlayEffect方法即可:AudioManager.PlayEffect("E5017")。又如,单击按钮时要播放音效,而对于按钮是委托事件,它比滑动面板时播放音效多了一层委托机制,所以正如前面那样,先建立AudioPlanyControl这个类,然后增加按钮的OnClick()事件,当按钮被单击时,OnClick事件发生,调用指定的AudioPlanyControl类中的方法PlayButtonAudioEffect(),该方法又会调用AudioManager中的PlayEffect方法,从而完成音效的播放。象这样绕来绕去的好处是可以方便我们管理,分类控制。
根据这个原理,下面我们继续来为前面已建立的两个调节音量的Slider增加处理事件:
 
根据前面的讲解,我们的设计思路是:在基类AudioManager中编写音量大小处理的具体方法,供调用类中的某个方法调用,而调用类又做为Slider控件的OnValueChanged(Single)事件的委托方,这样就达到了改变Slider滑块从而控制音量的目的了。

在AudioManager.cs中增加控制音量大小的静态方法(红色字部分即为增加的部分):
/*
*音频管理器
 */
using UnityEngine;
using System.Collections;
using System.Collections.Generic;                                //泛型集合命名空间

public class AudioManager : MonoBehaviour {
    public AudioClip[] AudioClipArray;                           //音频文件数组
static private Dictionary<string,AudioClip> _Dic;     //音频文件集合
    static private AudioSource[] AudioSourceArray; //背景音乐数组
static private AudioSource _AudioSource_backgroundMusic;     //背景音乐音频源
static private AudioSource _AudioSource_AudioEffect;         //音效音频源
// Use this for initialization
void Awake(){
//音频源集合类的加载处理
_Dic = new Dictionary<string,AudioClip> ();
foreach (AudioClip audioClipItem in AudioClipArray) {
            _Dic.Add(audioClipItem.name, audioClipItem);
}
//得到Audiosource
AudioSourceArray = this.gameObject.GetComponents<AudioSource> ();
_AudioSource_backgroundMusic = AudioSourceArray [0];
_AudioSource_AudioEffect = AudioSourceArray [1];
}
//播放背景音乐
public static void PlayBackground(AudioClip audiClip){
//不能与当前正在播放的背景音乐重复
if (_AudioSource_backgroundMusic.clip == audiClip) {
return;
}
//得到GlobalManger传过来的音量大小
_AudioSource_backgroundMusic.volume = GlobalManager.FloBackgroundAudioVolumns;

//播放
if (audiClip) {
_AudioSource_backgroundMusic.clip = audiClip;        //注入音频文件
_AudioSource_backgroundMusic.Play ();
} else {
Debug.LogError("背景音乐文件不能为空");
}
}
//播放背景音乐  方法重载  根据背景音乐文件的名称来播放
public static void PlayBackground(string StrAudioClipName){
if (!string.IsNullOrEmpty (StrAudioClipName)) {
PlayBackground(_Dic[StrAudioClipName]);
}else {
Debug.LogError("背景音乐文件不能为空");
}
}
//播放音效
    public static void PlayEffect(AudioClip audiClip)
    {

//得到GlobalManger传过来的音量大小
        _AudioSource_AudioEffect.volume = GlobalManager.FloAudioEffectVolumns;

//播放
if (audiClip) {
_AudioSource_AudioEffect.clip = audiClip;             //注入音效文件
_AudioSource_AudioEffect.Play ();
} else {
Debug.LogError("音效文件不能为空");
}
}
//播放音效音乐  方法重载  根据音效文件的名称来播放
    public static void PlayEffect(string StrAudioClipName)
    {
if (!string.IsNullOrEmpty (StrAudioClipName)) {
            PlayEffect(_Dic[StrAudioClipName]);
}else {
Debug.LogError("音效文件不能为空");
}
}
    //改变背景音乐音量
    public static void ChangeCurrentBackgroundAudioVolumsn(float floVolumses)
    {
        if (floVolumses >= 0 && floVolumses <= 1)
        {
            _AudioSource_backgroundMusic.volume = floVolumses;
            //改变GlobalMangerk中音量的值,让第1、2、....关卡均为此数值
            GlobalManager.FloBackgroundAudioVolumns = floVolumses;
        }
        else {
            Debug.LogError("音量值超范围,致使该值无意义");
        } 
    }
    //改变音效音量
    public static void ChangeCurrentAudioEffectVolumsn(float floVolumses)
    {
        if (floVolumses >= 0 && floVolumses <= 1)
        {
            _AudioSource_AudioEffect.volume = floVolumses;
            //改变GlobalMangerk中音量的值,让第1、2、....关卡均为此数值
            GlobalManager.FloAudioEffectVolumns = floVolumses;
        }
        else
        {
            Debug.LogError("音量值超范围,致使该值无意义");
        }
    }
}

GlobalManage为一个静态类,里面全是一些静态变量,是一个全局管理器,存储的值可应用于各个关卡,这个类在前面还没有编写,这里把它建立起来,其内容如下:
using UnityEngine;
using System.Collections;
public class GlobalManager : MonoBehaviour {
// 全局背景音量大小
    public static float FloBackgroundAudioVolumns = 1F;
    //全局音效大小
    public static float FloAudioEffectVolumns = 1F;
}

改变音量大小的方法我们已经在基类中建立起来了,其调用类我们可以利用原有的AudioPlanyControl类,在其中添加方法:
using UnityEngine;
using System.Collections;

public class AudioPlanyControl : MonoBehaviour {

// Use this for initialization
void Start () {

}

// 播放按钮音效
    public void PlayButtonAudioEffect()
    {
        AudioManager.PlayEffect("E5017");
    }
    //改变游戏背景音乐音量
    public void ChangeGameBackgroundVoluns(float floVolumn) {
        AudioManager.ChangeCurrentBackgroundAudioVolumsn(floVolumn);
    }
    //改变游戏音效音量
    public void ChangeGameAudioEffectVoluns(float floVolumn)
    {
        AudioManager.ChangeCurrentAudioEffectVolumsn(floVolumn);
    }
}
该脚本是_AudioPlayControl对象的组件,把_AudioPlayControl做为那两个控制背景音乐与音效音乐音量的Slider控件的事件委托对象,并选择事件对应的动态方法:
 
这样一来,当我们调节音量滑块时,OnValueChanged事件发生,就会调用指定的方法,并把滑块对应的值传进去:
//改变游戏背景音乐音量
    public void ChangeGameBackgroundVoluns(float floVolumn) {
        AudioManager.ChangeCurrentBackgroundAudioVolumsn(floVolumn);
    }
    //改变游戏音效音量
    public void ChangeGameAudioEffectVoluns(float floVolumn)
    {
        AudioManager.ChangeCurrentAudioEffectVolumsn(floVolumn);
    }
这个方法又会去调用AudioManager中的对应方法,同时继续把滑块对应的值传进去,在AudioManager中会把传进来的音量值赋给当前的背景音乐或音效,从而改变播放的音量,并存入全局变量里,控制整个程序中的音量:
//AudioManager.cs
……..
//改变背景音乐音量
    public static void ChangeCurrentBackgroundAudioVolumsn(float floVolumses)
    {
        if (floVolumses >= 0 && floVolumses <= 1)
        {
            _AudioSource_backgroundMusic.volume = floVolumses;
            //改变GlobalMangerk中音量的值,让第1、2、....关卡均为此数值
            GlobalManager.FloBackgroundAudioVolumns = floVolumses;
        }
        else {
            Debug.LogError("音量值超范围,致使该值无意义");
        } 
    }
    //改变音效音量
    public static void ChangeCurrentAudioEffectVolumsn(float floVolumses)
    {
        if (floVolumses >= 0 && floVolumses <= 1)
        {
            _AudioSource_AudioEffect.volume = floVolumses;
            //改变GlobalMangerk中音量的值,让第1、2、....关卡均为此数值
            GlobalManager.FloAudioEffectVolumns = floVolumses;
        }
        else
        {
            Debug.LogError("音量值超范围,致使该值无意义");
        }
    }
至此,整个工程的主体结构如下图:

Unity UGUI开发设计及案例讲解相关推荐

  1. 码农人生——从未学过Android如何开发Android App 案例讲解-第002期案例

    标题有点晃眼,本次分享是002期博文的实践故事,不会有任何代码.也不会教别人android 如何开发,类似博文已经有大批大批,而且还会有陆陆续续的人写,我写的文章,主要是经验之谈,希望总结出的一些方法 ...

  2. 视频教程-Oracle数据库开发技巧与经典案例讲解一-Oracle

    Oracle数据库开发技巧与经典案例讲解一 Oracle DBA,熟悉Unix操作系统,精通Oracle数据库. 曾任职某大型金融IT公司,负责银行领域数据库构建与运维,维护大量银行数据库系统.目前在 ...

  3. 【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

    文章目录 一.前言 二.关于Skynet 三.Ubuntu虚拟机 1.Ubuntu系统镜像下载 2.VirtualBox虚拟机软件 2.1.VirtualBox下载 2.2.VirtualBox安装 ...

  4. 跟着王进老师学开发Python篇:基础强化案例讲解-王进-专题视频课程

    跟着王进老师学开发Python篇:基础强化案例讲解-143人已学习 课程介绍         共计27个项目案例+项目源码,跟着王进老师尽情玩转Python解释器! 案例涵盖的内容有:Python程序 ...

  5. 跟着王进老师学开发Python篇:基础入门案例讲解-王进-专题视频课程

    跟着王进老师学开发Python篇:基础入门案例讲解-166人已学习 课程介绍         共计45个项目案例+项目源码,跟着王进老师尽情玩转Python解释器! 本课程涉及Python的基础语法, ...

  6. 视频教程-跟着王进老师学开发Python篇:基础入门案例讲解-Python

    跟着王进老师学开发Python篇:基础入门案例讲解 教学风格独特,以学员视角出发设计课程,难易适度,重点突出,架构清晰,将实战经验融合到教学中.讲授技术同时传递方法.得到广大学员的高度认可. 王进 ¥ ...

  7. unity应用开发实战案例_Unity开发实战游戏教学案例分享

    进行项目实战是快速入门或提升Unity开发的关键.Asset Store资源商店中,有大量完整项目模板和教学案例,帮助您通过项目实战,让你体会到Unity开发的成就感. 本文我们为大家准备了三款实战游 ...

  8. 航空专场 | 无人机设计仿真流程讲解与案例实操

    一.CFD在无人机上的应用 1.静.动气动系数计算以上介绍的无人机的流动状态一般为中低雷诺数,不可压缩流动.这些计算一般用S-A模型或者KW-SST模型进行计算,能够获得不错的工程精度.静.动气动力系 ...

  9. 中医药人工智能-知识图谱-开发设计案例

    中医药人工智能-知识图谱-开发设计案例 ●概述: <智慧中医药大脑>是基于知识工程技术,应用 人工智能算法开发的:面向医生.医学生和中医药爱好者,用于学习科研目的的中医药"辨证论 ...

最新文章

  1. Windows Server 2012 R2 虚拟机迁移 出错 21502 0x80070490 解决
  2. HTML与CSS基础之伪类选择器(三)
  3. P5437-[XR-2]约定【拉格朗日差值,数学期望】
  4. java 1.7 新特性
  5. 华为背锅?微博大V质疑华为P30 Pro拍月亮造假 公司称误导观众已开除
  6. 在iphone上安装多个微信 【微信营销必备】
  7. dfs记忆化搜索(带限制的选择问题) 讲解:LeetCode打家劫舍||| / 蓝桥 地宫取宝/蓝桥 k进制数//剪格子//方格分割
  8. Spring Security 工作原理概览
  9. windows下的Zcash钱包(ZEC钱包)-zcash4win 1.0.12
  10. 手把手教你在 SpringBoot 自定义参数解析器
  11. html中显示框框中对勾,如何打出方框里有对勾
  12. 浙江省乡村快递寄件数据分析-快递100百递指数
  13. 7分钟学会HTML网页制作
  14. clover删除多余引导_clover如何删除无用启动项_常见问题解析,clover
  15. 客户端如何修改服务器时间设置在哪里看,客户端同步服务器时间设置在哪里
  16. Dev-C++如何更改字体大小
  17. 【无标题】如何重置密码
  18. Python学习笔记之 中英文文本情感分析
  19. 图书管理系统之带验证码登录界面
  20. 开源项目与J2EE架构介绍

热门文章

  1. MacBook好用软件分享(长期更新)
  2. PCR实验室应该怎样布置呢?
  3. 致2018届毕业生的公开信:计算机科学的三堂人生课
  4. # 英文从零开始写作---常见问题和技巧csdn
  5. 侠客风云传未能连接到服务器,《侠客风云传:前传》无法启动解决方法
  6. linux系统能按k宝驱动程序,农行K宝不能使用 农行K宝导致光驱无法识别的解决方法...
  7. c语言 李敬兆 课后答案,C语言程序设计习题与实验指导
  8. GNN和GGNN学习笔记
  9. Linux的学习心得和知识总结 第一章(完)
  10. 【软件测试 #1】策略练习题