UGUI实现窗口切换动画

前言

在开发的过程中我们常常需要创建很多UI窗口,如何让这些窗口按照递归的方式而且还是带动画地进行切换呢,本文介绍了一种方法实现这个功能

思路

为了实现这样的效果,需要搭建一个UI框架,原理就是让Animator组件控制红框UI容器移动

在红框容器中再放置两个空物体作为子容器,分别是A和B,A是当前的页面,B是要打开的窗口,而B子容器部分还可再嵌套一个容器,这样移动红框容器,就可以将B部分显示出来

这时如果B部分也是一个容器,那么再移动B部分就能将C显示出来,以此类推

功能实现

首先要实现的就是创建控制父容器的两个动画,移动和恢复,向左移动以显示出B子容器,向右恢复来显示A子容器,使用Ctrl+6 打开动画设置面板,选中红框容器,为其创建一个动画,创建动画的过程在这里就不赘述了,只是简单的移动Anchored Position位置,然后在Animator中为其添加展开和关闭动画,并设置is_Open bool用于值切换动画,一切准备好后,就可以为红框父容器添加一个脚本:Container 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/// <summary>
/// 红框容器脚本
/// </summary>
public class Container : MonoBehaviour
{
Animator at;
public Container before; //上一个容器
public Button btn_back; //返回上一个容器按钮(关闭当前窗口)

public Transform A; //子容器A
public Transform B; //子容器B

private void Awake()
{
at = GetComponent<Animator>(); // 获取动画组件
A = transform.Find("A"); //获取子容器
B = transform.Find("B");
if(btn_back!=null)
btn_back.onClick.AddListener(Back);//按钮绑定事件
}
//设置当前是否播放动画,参数为true时,切换到B,false时切换回A
public void SwitchWindow(bool b)
{
at.SetBool("isOpen", b);
}
//按下返回按钮
public void Back()
{
before.SwitchWindow(false); //关闭当前窗口时,让父容器切换回A页面
//返回到上一个窗口时,将自身从父容器的B子容器中分离,因为父窗口不一定只会打开这个窗口,可能会有多个分支
transform.parent= null;
gameObject.SetActive(false); //隐藏当前窗口
}
}

这样,只需要调用SwitchWindow(bool b)方法,就能实现切换窗口,接下来要创建打开一个新窗口的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class OpenWindow : MonoBehaviour
{
Container Con; //当前容器
public Transform win; //下一个窗口
public Button btn; //打开按钮
private void Awake()
{
Con = GetComponent<Container>(); //获取当前容器组件
btn.onClick.AddListener(() =>
{
win.parent = Con.B; //要显示的窗口设置为容器的B
(win as RectTransform).anchoredPosition = Vector3.zero;//恢复0位置,相当于B容器
win.gameObject.SetActive(true); //激活显示要切换的窗口
win.GetComponent<Container>().before = Con; // 将当前容器传递给下一个容器
Con.SwitchWindow(true); //切换到B
});
}
}

将这个脚本赋予给红框容器,然后添加打开下一个页面的按钮和下一个窗口的Transform,默认情况下要打开窗口的GameObject是禁用隐藏的,然后先将它放入当前容器的B子容器中(预备打开),将其位置归零,然后激活这个GameObj,把当前窗口的Container组件传递给新窗口(用于在新窗口关闭时调用),一切都准备就绪后,使用SwitchWindow(true)让当前红框容器动画切换到B子容器,这时控制权为B容器中的内容,是调用Back()方法返回上一个页面,还是再进入新的页面都可以。

结构图

最后Ctrl+D复制几个容器,主容器上添加OpenWindow脚本,把复制出来的容器拖入即可

总结

实现这个技术主要用到了嵌套的模式结合动画来实现这个效果,有点类似于数据结构中的链栈结构,可以不断的打开新的窗口,但回退时必须要原路返回

Git快速上手

什么是Git

git是一个版本控制软件,它通过创建一个本地仓库(通俗点讲就是一个特殊文件夹),你可以将项目文件放置在包含仓库的文件夹中,当新增了项目代码或者修改后,你可以提交到仓库来保存一个版本,每当项目新增内容或者修改时,可以再次提交,这时git会帮助你保留两个版本之间的差异,在多人协作项目时可以很好的管理。

安装Git

访问官网 https://git-scm.com/ 下载并安装
使用默认安装完成后,在文件夹中右键,会出现一下两个新选项

  • Git GUI Here
  • Git Bash Here
    你也可以在bash/cmd中使用git --version命令来查看当前安装版本

入门

结构图

工作区就是日常的项目所在文件夹,而使用git的区别就是,它会在这个文件夹中创建一个.git的隐藏文件夹,这就是本地仓库。

UI自适应分辨率

前言

有时候我们排版好UI布局后,虽然相对位置可以随分辨率改变而改变,但图像的尺寸则是固定的,分辨率低时图形几乎占满画面,无法使用,那么就需要设置UI图形自适应缩放了

设置方法

修改Canvas对象的Canvas Scaler组件属性:UI Scale Mode改为
Scale With Screen Size
并设置参考分辨率
这个Reference Resolution的意思是在这个分辨率下,UI元素显示的比例/尺寸是你想要的效果,或者你就是按照这个分辨率/比例调节的界面元素尺寸,Unity根据这个标准的分辨率来推算不同分辨率下UI元素的缩放程度,而程度是由Match属性决定的,值越小,调节的幅度越大。

Unity查找对象的几种方法

前言

在Unity开发中经常需要获取游戏对象,获取的方法有很多种本文就列举几种常用的方法

首先要了解两个类

  • GameObject
  • Transform
    GameObject表示的是游戏对象,Transform也可以表示游戏对象,它们之间可以互相转化
1
2
Transform t = gameobject.transform;
GameObject go = t.gameobject;

而它们的区别在于:GameObject更倾向于游戏体本身的属性,如物体激活状态,物体的名称,物体的标签,物体的渲染层等设置,而Transform更多是表述模型的层级关系也就是父子关系,和本身的位置旋转缩放等信息,所以当需要查找嵌套的物体时,优先使用Transform来查找模型,GameObject更适合在不关心物体层级关系的情况下查找,且通常是使用GameObject的静态方法查找,Transform则更多调用实例方法。

通过GameObject获取

  1. 已经知道物体的名称,并且它是唯一的
    GameObject.Find("对象名")
  2. 已经知道物体的标签,并且是唯一的
    GameObject.FindWithTag("标签名")
  3. 已经知道物体的标签,且有多个,返回一个GameObject数组
    GameObject.FindGameObjectsWithTag("标签名")

以上方法只能查找激活状态的物体

通过Transform获取

  1. 需要获取当前物体下的某个子物体(脚本挂载当前物体上)
    transform.Find("直接子物体的名称 or 子物体路径")
    如果需要查找的物体就在当前物体下,可以直接使用物体名称,如果需要查找物体在更深的位置(当前是a物体,其下有b物体,b下有c物体),需要通过路径:”b\c”来找到c物体。
  2. 查找所有包含组件Button的物体
    Transform.FindObjectsOfType<Button>()

Transform查找物体的好处是,它能找到被禁用(未激活)的物体

Unity中的单例基类

前言

在开发过程中,由于游戏项目功能模块繁多,很多独立的模块都有自己单独的唯一入口,基本上都是用单例实现的,就像

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UIManager : MonoBehaviour
{
private static UIManager _instance;
public static UIManager Instance
{
get
{
if (_instance == null)
_instance = new GameObject(typeof(UIManager).Name).AddComponent<UIManager>();
return _instance;
}
}
}

上面的UIManager就是一个单例类,当第一次访问UIManager.Instance的时候,我们就会在游戏内创建一个名叫“UIManager”的物体,它挂载了我们的单例脚本实例UIManager。
现在有一个问题,假如我有100个单例类要创建,每个类都要复制这段代码很麻烦很不精炼,有没有更清爽的做法?
当然有,我们把单例的实现封装到基类中去就能省去代码复制之苦,可是基类并不知道子类的具体类型,怎么返回子类型的Instance?如果Instance是以基类类型返回的,那么这个基类的存在意义就不大了。但是幸好C#有泛型,我们可以把子类的具体类型以泛型参数T的形式通知给基类,这样基类就能返回子类型。下面是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = new GameObject(typeof(T).Name).AddComponent<T>();
}
return _instance;
}
}
}

这个基类最重要的代码就是where T : Singleton,它限定T必须是Singleton的子类,虽然它长的着实有点奇怪。
这时我们再声明单例类UIManager的时候就可以一行代码搞定了:

1
2
3
public class UIManager : Singleton<UIManager>
{
}

这里子类把自己的类型作为泛型参数通知给了基类,基类便知道Instance应该是什么类型了。使用的时候直接访问UIManager.Instance即可。
这个Singleton我觉得有点复杂,同时创建了一个私有静态变量_instance和公有静态属性Instance,显得有点冗余,这里我们做个优化:

1
2
3
4
public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
public static readonly T Instance = new GameObject(typeof(T).Name).AddComponent<T>();
}

OOP接口的理解

概括

最终目标:以不变应万变。
类与类之间用接口引用,而不是直接调用,接口派生类再怎么变化只要接口不改程序就不会出现问题。
把多个类似的类的相同/主要操作提取出一个接口并实现,然后这些类抽取出相同因素构成父类,令接口使用泛型限制为父类的类型,既满足了多各类的相同方法实现,也满足相同方法中的不同类型的参数。

主要逻辑原则,基本业务可以抽象成任务接口,业务类继承它,在控制器层调用任务并实例化相应的业务类,可以直接简单的操作,即使以后业务内容变动,控制器层仍然不需要修改。

不同的层展示不同的方法,通过接口就可以区分层次,高层的类(控制器)无需知道低层类(业务)的其他方法,只要使用业务.执行()的方法,同样业务类也无需知道sql查询过程,只管提交数据给dao对象查询结果,如果通用dao接口能满足使用,就无需再向下转型。
控制器 -(任务接口)- 业务 -(dao接口)- 数据访问实现类

接口是完全公开,公有的,目的就是让所有人都可以使用,但使用的人并不需要关心它内部是如何实现的,只需要使用接口固定的几个关键方法,只需知道怎么使用,不关心原理,这就是接口的意义。

例如一个USB接口,带有USB接口的设备成千上万,但都通过这一个接口连接电脑,对于电脑(调用方)来说,并不关心这个设备(被调用方)是怎么实现它的功能的,我只关心怎么和它通信,这就要通过接口来实现,如USB接口中定义了两个方法,send(),get() send方法用来向USB设备发送数据,而get方法用来接收数据,比如这是个u盘设备并实现了USB接口,那么电脑只需要调用uDisk.send(File)方法传入某文件/数据,这样就可以把文件写入u盘中,而电脑根本不用知道u盘内部是如何存储文件,如何管理文件的,又比如这个USB设备是一个鼠标,我们马上会想到使用Info i = mouse.get()通过get获取鼠标发回来的信息,并进行解析,看到这里,不难发现,不管是何种USB设备,它和电脑之间永远只有两种方法,就是发送和接收,这就是将两个对象间的调用关系简化到极致的产物,它已经不能再简单了,我们只要把这些关键方法写作一个接口,而它们之间交互的数据类型则成为USB接口的泛型,发送和接收方法的参数/返回值类型就可以是泛型的,这样就能使得他们之间的联系变得有条不紊。

这些设备知道自己该做什么,但都遵循这个USB接口规则*

Vim快速上手

前言

Vim是Linux系统下一款非常好用的编辑器,用的好会非常顺手,而不会用会让你想把电脑砸了,下面就来介绍介绍它

首先要了解的是它有三种状态分别是:

  • 命令模式:控制光标移动,对文本进行复制粘贴、删除和查找等工作
  • 输入模式:正常的文本输入,类似于记事本
  • 末行模式:保存或退出文档,以及设置编辑环境,增加行号显示灯操作

以下是三种模式状态的状态切换图

>命令模式

Vim在进入时自动进入命令模式,命令模式主要是对文本的一些编辑操作,其中内置的命令也有几百上前种用法,下面是一些常用命令,通常该模式用来复制粘贴文本内容

命令 作用
dd 删除(剪切)光标所在整行
5dd 删除(剪切)从光标处开始的5行
yy 复制光标所在行
5yy 复制光标所在处开始的5行
v 开始选择范围(选择后按y或d操作)
V 开始选择行范围
n 显示搜索命令定位到的下一个字符串
N 显示搜索命令定位到的上一个字符串
u 撤销上一步操作
p 将剪切的内容粘贴到光标位置
h 左移光标
j 下移光标
k 上移光标
l 右移光标
多行缩进:
按v选择要缩进的行,然后按‘>’来缩进‘<’来取消缩进

>末行模式

末行模式主要用于保存或退出文件,和对vim进行一些配置,还可以让用户执行外部的Linux命令跳转到所编写文档的特定行数。要想切换到末行模式,只需要在命令模式下敲一个‘:’就可以进入

命令 作用
:w 保存文件
:q 退出编辑器
:wq/ZZ/:x 保存并退出
:q! 强制退出(放弃对文档的修改内容)
:wq! 强制保存退出
:set nu 显示行号
:set nonu 关闭行号
:命令 执行命令
:整数 跳转到该行
: s/a/b 将当前光标所在行中的第一个a替换成b
: s/a/b/g 将当前光标所在行中的所有a替换成b
: %s/a/b/g 将全文中的所有a替换成b
? 关键字 在文本中搜索关键字(从下至上)
/ 关键字 在文本中搜索关键字(从上至下)
Vim始终显示行号的方法:
定位到home目录,使用命令 vi .vimrc 创建配置文件,在配置文件中写入set nu保存退出即可

>输入模式

进入输入模式的方法,是在命令模式下按 a、i、o 键,其中 a 是在当前光标位置后移一位的位置开始插入文本,i 是在当前光标位置开始插入文本,o 键是在当前光标所在行下再新起一行开始输入

技巧

连续插入字符:在命令模式下,输入数字,然后按 i 进入插入模式,输入一个字符再按ESC键就能连续插入这个字符

MarkDown教程

一级标题

二级标题

一级标题

二级标题

三级标题

四级标题

五级标题
六级标题

斜体文本
斜体文本
粗体文本
粗体文本
粗斜体文本
粗斜体文本

分割线






RUNOOB.COM
GOOGLE.COM
BAIDU.COM

下划线

使劲按^sj

  • 无序列表
  • 无序列表
  • 无序列表
  • list
  • list
  • list
  • list
  • list
  • list
  1. 有序列表
  2. 有序列表
  3. 有序列表
    • 嵌套列表
    • 嵌套列表
    • 嵌套列表

引用内容一级

引用内容二级

引用内容三级

  • 引用嵌套列表
  • 引用嵌套列表
  1. 有序列表

    列表嵌套引用

  2. 有序列表

正文内容

print(float f)函数

黑色背景块
黑色背景块
黑色背景块
黑色背景块
1
2
3
4
代码片段
$(document).ready(function () {
alert('RUNOOB');
});
1
Console.Readkey("JJ");

link1: aa
link2: 1

RUNOOB 图标

表格
| A | B | C |
| — | — | — |
| 第一列 | 第二列 | 第三列 |