Unity3D 单例模式和静态类的使用详解

Unity3D的API提供了很多的功能,但是很多流程还是会自己去封装一下去。当然现在网上也有很多的框架可以去下载使用,但是肯定不会比自己写的用起来顺手。

对于是否需要使用框架的问题上,本人是持肯定态度的,把一些常用方法进行封装,做成一个功能性的框架,可以很大程度上提高代码的效率,维护也方便。

对于网络上很多教程上使用的“游戏通用MVC框架”,现在看来并不符合MVC这种结构性框架的设计思想:要知道,MVC最初是被设计为Web应用的框架,而游戏中的很多事件并不是通过用户点击UI发生的,View和Controller在游戏逻辑中的占比一般都少的可怜,而且很多教程上把Model剥离出很多“Manager”模块,甚至有人把View和Controller合在一起写了UIManager——连MVC的结构都没了,为啥还要称之为MVC框架呢?

MVC: “人红是非多。。。。”

目前大部分的游戏框架——特别是小型项目的游戏框架——都是把一些数据的特定行为进行了一下封装:生成一个物件,播放一个特效,进行一次随机事件等。当然也会有一些结构性的设计或者资源管理设计如:UI的回退栈或者回退链,场景的载入记录和切换,下载队列的管理等。

在Unity的框架设计中,有一个词会经常见到:单例模式(singleton)。单例模式就是在整个游戏中只使用某个类的一个实例,核心的一句话就是public static T Instance;即在类中定义了一个静态的自身实例供外部使用,调用方法时就是:T.Instance.Function()。在本人最初接触这种设计方式时经常会与静态类弄混淆,T.Function()。中间差了一个静态Instance,很多时候好像区别不大。。。

在接近两周左右的时间里,我一直在纠结于自己正在写的框架到底应该写成单例模式的还是静态模式的,今天刚好对这个问题有了一个新的想法:静态可不可以理解为一种封闭性很强的单例?

首先回想一下静态的两个常识:

1、静态类不能继承和被继承!(严格点说是只能继承System.Object)也就是说你的静态类不可能去继承MonoBehaviour,不能实现接口。

2、静态方法不能使用非静态成员!如果你大量使用静态方法,而方法里又需要用到这个类的成员,那么你的成员得是静态成员。

第2点需要注意:如果你想在Unity的编辑器下调整某个参数,那么这个参数就不能是静态的(哪怕你自定义EditorWindow去修改这个值也没用),解决的办法是通过UnityEngine.ScriptableObject去存放配置(生成*.asset文件),然后在运行中通过LoadAsset去加载,然后再改变静态成员。至于原因,相信不难理解——你看到的所有Unity组件都是一个个实例,你要通过Unity的编辑器去配置,那么你就得有一个这样的可配置实例。

从面向对象上想一下:静态方法或者静态类,不需要依赖对象,类是唯一的;单例的静态实例,一般就是唯一的一个对象(当然也可以有多个)。差别嘛。。。好像也不大。。。

如果这样考虑没有错,那再回头比较一下两种方式:

1、静态(静态方法或者静态类),代码编写上绊手绊脚,方法调用很方便,运行效率高一丢丢。逻辑面向过程,不能很好地控制加载和销毁。

2、单例(类的静态实例),代码编写和其他类完全一样,继承抽象模版接口都可以,Unity里也很方便进行参数配置,不过使用麻烦有犯错的可能性(必须通过实例调用方法),效率不如静态(但是也不会有很大影响吧)。

如果这些说法太抽象,那我再给出一个常见的问题:如果你的框架有一个SoundManager能够管理所有的声音播放,那么你会怎么去实现?

(在刚接触AudioSource这个组件的时候,我想的是每一个声音都由一个AudioSource去播放。但是后来发现完全没必要,AudioSource有静态的PlayClipAtPoint方法去播放临时3D音效,同时有实例方法PlayOneShot去播放临时音效(2D和3D取决于当实例的SpatialBlend)。如果没有特殊的需求,那么一个AudioSource循环播放背景音乐,上述两种方法播放游戏中的特效音频,这对于大部分游戏已经足够了。)

那么问题来了:你的SoundManager播放声音的方法如果是静态的,那么AudioSource组件必须在代码中通过各种方式去获取(新建组件或者获取特定GameObject下的组件)——因为保存这个组件的变量必须是静态的,也就不能通过Unity的编辑器去赋值。如果不去阅读代码那么用户完全不知道这是一个什么样的组件获取流程,如果我破坏这个流程(同名物体,包含互斥组件等),那么这个Manager很有可能会出现不可预料的异常。

而继承MonoBehaviour并RequireComponent(typeof(AudioSource)),怎么看也比“为了静态而静态”的代码要方便健壮的多。

实际上到这里已经可以基本总结出何时需要使用单例了:

1、只要你的类需要保存其他组件作为变量,那么就有必要使用单例;

2、只要你有在Unity编辑器上进行参数配置的需求,那么就有必要使用单例;

3、只要你的管理器需要进行加载的顺序控制,那么就有必要使用单例(比如热更新之后加载ResourcesManager);

当然,这里都只是“有必要”,并不是“必须”。两者区别最大的地方,一个是方便写,一个是方便用。方便写的代价是每次调用加个instance,方便用的代价则是放弃了面向对象和Unity的“所见即所得”,孰轻孰重,自己抉择。

另一方面,和“为了静态而静态”一样,“为了单例而单例”同样是一个不合理的设计。这样的解释仍然是那么的模糊,那么,就给自己定义一个最简单的规则吧——如果你的单例类里没有任何需要保存状态的变量,那么这个类里的方法就可以全都是静态方法,这个类也可以是个静态类。

补充:从实例出发,了解单例模式和静态块

就算你没有用到过其他的设计模式,但是单例模式你肯定接触过,比如,Spring 中 bean 默认就是单例模式的,所有用到这个 bean 的实例其实都是同一个。

单例模式的使用场景

什么是单例模式呢,单例模式(Singleton)又叫单态模式,它出现目的是为了保证一个类在系统中只有一个实例,并提供一个访问它的全局访问点。从这点可以看出,单例模式的出现是为了可以保证系统中一个类只有一个实例而且该实例又易于外界访问,从而方便对实例个数的控制并节约系统资源而出现的解决方案。

使用单例模式当然是有原因,有好处的了。在下面几个场景中适合使用单例模式:

1、有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式;

2、创建对象时耗时过多或者耗资源过多,但又经常用到的对象;

3、频繁访问 IO 资源的对象,例如数据库连接池或访问本地文件;

下面举几个例子来说明一下:

1、网站在线人数统计;

其实就是全局计数器,也就是说所有用户在相同的时刻获取到的在线人数数量都是一致的。要实现这个需求,计数器就要全局唯一,也就正好可以用单例模式来实现。当然这里不包括分布式场景,因为计数是存在内存中的,并且还要保证线程安全。下面代码是一个简单的计数器实现。

public class Counter {
    private static class CounterHolder{
        private static final Counter counter = new Counter();
    }
    private Counter(){
        System.out.println("init...");
    }
    public static final Counter getInstance(){
        return CounterHolder.counter;
    }
    private AtomicLong online = new AtomicLong();
    public long getOnline(){
        return online.get();
    }
    public long add(){
        return online.incrementAndGet();
    }
}

2、配置文件访问类;

项目中经常需要一些环境相关的配置文件,比如短信通知相关的、邮件相关的。比如 properties 文件,这里就以读取一个properties 文件配置为例,如果你使用的 Spring ,可以用 @PropertySource 注解实现,默认就是单例模式。如果不用单例的话,每次都要 new 对象,每次都要重新读一遍配置文件,很影响性能,如果用单例模式,则只需要读取一遍就好了。以下是文件访问单例类简单实现:

public class SingleProperty {
    private static Properties prop;
    private static class SinglePropertyHolder{
        private static final SingleProperty singleProperty = new SingleProperty();
    }
    /**
    * config.properties 内容是 test.name=kite
    */
    private SingleProperty(){
        System.out.println("构造函数执行");
        prop = new Properties();
        InputStream stream = SingleProperty.class.getClassLoader()
                .getResourceAsStream("config.properties");
        try {
            prop.load(new InputStreamReader(stream, "utf-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static SingleProperty getInstance(){
        return SinglePropertyHolder.singleProperty;
    }    

    public String getName(){
        return prop.get("test.name").toString();
    }
    public static void main(String[] args){
        SingleProperty singleProperty = SingleProperty.getInstance();
        System.out.println(singleProperty.getName());
    }
}

3、数据库连接池的实现,也包括线程池。

为什么要做池化,是因为新建连接很耗时,如果每次新任务来了,都新建连接,那对性能的影响实在太大。所以一般的做法是在一个应用内维护一个连接池,这样当任务进来时,如果有空闲连接,可以直接拿来用,省去了初始化的开销。

所以用单例模式,正好可以实现一个应用内只有一个线程池的存在,所有需要连接的任务,都要从这个连接池来获取连接。

如果不使用单例,那么应用内就会出现多个连接池,那也就没什么意义了。如果你使用 Spring 的话,并集成了例如 druid 或者 c3p0 ,这些成熟开源的数据库连接池,一般也都是默认以单例模式实现的。

单例模式的实现方法

如果你在书上或者网站上搜索单例模式的实现,一般都会介绍5、6中方式,其中有一些随着 Java 版本的升高,以及多线程技术的使用变得不那么实用了,这里就介绍两种即高效,而且又是线程安全的方式。

1. 静态内部类方式

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

这种写法仍然使用 JVM 本身机制保证了线程安全问题,由于 SingletonHolder 是私有的,除了 getInstance() 方法外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。上面的两个例子就是用这种方式实现的。

2. 枚举方式

public enum SingleEnum {
    INSTANCE;
    SingleEnum(){
        System.out.println("构造函数执行");
    }
    public String getName(){
        return "singleEnum";
    }
    public static void main(String[] args){
        SingleEnum singleEnum = SingleEnum.INSTANCE;
        System.out.println(singleEnum.getName());
    }
}

我们可以通过 SingleEnum.INSTANCE 来访问实例。而且创建枚举默认就是线程安全的,并且还能防止反序列化导致重新创建新的对象。

静态块

什么是静态块呢

1、它是随着类的加载而执行,只执行一次,并优先于主函数。具体说,静态代码块是由类调用的。类调用时,先执行静态代码块,然后才执行主函数的;

2、静态代码块其实就是给类初始化的,而构造代码块是给对象初始化的;

3、静态代码块中的变量是局部变量,与普通函数中的局部变量性质没有区别;

4、一个类中可以有多个静态代码块;

他的写法是这样的:

static {
        System.out.println("static executed");
    }

来看一下下面这个完整的实例:

public class SingleStatic {
    static {
        System.out.println("static 块执行中...");
    }
    {
        System.out.println("构造代码块 执行中...");
    }
    public SingleStatic(){
        System.out.println("构造函数 执行中");
    }
    public static void main(String[] args){
        System.out.println("main 函数执行中");
        SingleStatic singleStatic = new SingleStatic();
    }
}

他的执行结果是这样的:

static 块执行中...

main 函数执行中

构造代码块 执行中...

构造函数 执行中

从中可以看出他们的执行顺序分别为:

1、静态代码块

2、main 函数

3、构造代码块

4、构造函数

利用静态代码块只在类加载的时候执行,并且只执行一次这个特性,也可以用来实现单例模式,但是不是懒加载,也就是说每次类加载就会主动触发实例化。

除此之外,不考虑单例的情况,利用静态代码块的这个特性,可以实现其他的一些功能,例如上面提到的配置文件加载的功能,可以在类加载的时候就读取配置文件的内容,相当于一个预加载的功能,在使用的时候可以直接拿来就用。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。如有错误或未考虑完全的地方,望不吝赐教。

(0)

相关推荐

  • Unity3D运行报DllNotFoundException错误的解决方案

    起因 unity程序build到pc上,拿到其他人的机器上结果有些功能不正常,看log里面大概是 Fallback handler could not load library: xxx.dll DllNotFoundException: xxx.dll 初看以为是缺失dll,但是实际上并不是这样,首先在很多人机器上都是没有问题的,只在极少数机器上才出现异常,另外报错的dll都是有的,并不存在缺失的问题. 后来网上搜了一下,看到http://answers.unity3d.com/questio

  • C#中public变量不能被unity面板识别的解决方案

    究其根本,原因在于,能在unity面板上识别的变量,public不是唯一的条件,另外一个条件是可序列化 比如你声明了如下一个类 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Wave { public GameObject prefab; public int count; public float rate; } 在另外一个类中进行声明时如下: public

  • 在Unity中使用全局变量的操作

    创建一个脚本,输入下面的代码.脚本不需要添加到任何物体上,globle变量可以跨场景全局调用. using System.Collections; using System.Collections.Generic; using UnityEngine; public class Globle { public static int globle= 1; } 和以前的许多语言不同,C#没有全局变量或全局函数.C#的所有字段和方法都在类的上下文中. 在C#中,与全局字段或函数等价的是静态字段或方法.

  • Unity 读取文件 TextAsset读取配置文件方式

    1 支持文件类型 .txt .html .htm .xml .bytes .json .csv .yaml .fnt 2 寻找文件 1 //Load texture from disk TextAsset bindata= Resources.Load("Texture") as TextAsset; Texture2D tex = new Texture2D(1,1); tex.LoadImage(bindata.bytes); 2 直接在编辑器中赋值 public TextAsse

  • Unity游戏之存储数据

    在许多游戏中当我们因为一些问题无法接着进行游玩,我们都会选择保存,以便后面有空时,接着游玩.接下来,我们会学习一些Unity有关的存储方法. 一.Unity-PlayerPrefs(数据持久化) 这是Unity自带的用于本地持久化保存与读取的类,采用的是键值对的方式来进行存储,一般通过键名来进行获取.PlayerPrefs有Int,float,string类型. 保存数据 PlayerPrefs.SetString("Color", color); PlayerPrefs.SetInt

  • Unity 静态变量跨场景操作

    创建两个场景同时赋值StaticVarious 脚本 然后按键好,H ,J 进行不断切换场景,会发现unity 控制台输出数字不断增加,然后把静态去掉,这样结果都是10. using UnityEngine; using System.Collections; using UnityEngine.SceneManagement; public class StaticVarious : MonoBehaviour { static int value = 10; void Start () {

  • Unity3d 如何更改Button的背景色

    我就废话不多说了,大家还是直接看代码吧~ using UnityEngine; using System.Collections; public class ButtonStyle : MonoBehaviour { public Color _color;//在编辑环境下选择背景色,透明度不能为0 public Texture2D tex; void OnGUI(){GUI.Button(new Rect(0,0,100,100),"tex");Color oldColor = GU

  • Unity3d使用FairyGUI 自定义字体的操作

    最近ui同学使用了一种新字体(锐字锐线怒放黑简) 发现全部切成图片字体 吓死我了 unity3d和fairygui搭配使用字体过程 1.第一步肯定是找美术同学拿他们找来的字体 由于fairygui编辑器用的字体是访问操作系统的字体 所以拿到新字体直接双击安装吧(或者放入操作系统盘内的Windows/Fonts)如下图 2.在fairygui里加个文本 选择字体 就能找到我们添加的字体了 3.接着把字体拷贝到unity3d工程下 存放在Resource\Font(没有可以自己建文件夹)注意这边命名

  • Unity使用物理引擎实现多旋翼无人机的模拟飞行

    内容简介 最近在用Unity实现无人机的模拟飞行,但发现站里基本没有完整介绍如何实现该功能的博客,因时间紧迫,就自己简单做了一个仿真(不是完全按照现实物理情景来做,即通过各个螺旋桨旋转产生力带动机体飞行).下面我会简述完全按现实物理情景实现模拟飞行,并详细描述我自己做的模拟飞行(不完全仿真),给各位提供参考. 现实物理情景的实现--简述(以四旋翼无人机为例) 因为我没有实现1:1仿真,所以这里只介绍思路,希望能给到读者一点启发. 构建一个四旋翼无人机模型(可以网上下载.或用Maya等建一个) 将

  • 解决在Unity中使用FairyGUI遇到的坑

    首先!首先!首先! 首先,我们由于历史问题,项目用的UI编辑器不是大众使用的GUI或者NGUI, 而是使用不知道算不算小众的FairyGUI,这个UI系统使用挺方便的,也提供了很多UI编码的案例,至少从直接使用来说方便了不少. 但是!但是!但是! 可能用到这个UI编辑器的不是那么多,项目上遇到的问题在网上百度出来的结果很少,基本自己断点查找bug. 最后!最后!最后! 我这个从没写过几次技术博客的人,要写这篇技术贴的原因是:昨晚加班到四五点钟查找BUG回到屋里,发现由于没有关好自己卧室的门,室友

  • unity 切换场景不销毁物体问题的解决

    在用unity进行游戏开发时我们有时需要一些物体在场景切换时不需要被销毁这时我们可以用官方给的DontDestroyOnLoad()方法, 这个方法可以让我们在场景切换时不销毁场景.但如果你又返回这个场景(创建不可销毁物体的场景)时就会发现会有两个这个物体(标记为不可销毁的物体). 这个问题有一个较为简单的解决方法就是: 在物体的Awake()或Star()方法中加入这段代码.如果找到和自己一样的名字却不是自己的物体就将自己销毁,这样就解决了这个问题.(别的地方不知道,我的项目是可以用): if

  • 在unity脚本中控制Inspector面板的参数操作

    在编写脚本的过程中我们会遇到一些小问题 比如一个的变量 为了在其他脚本中可以调用 我们需要写成public类型的 这样的话在Inspector面板中会出现此变量 这篇博客会给大家介绍一些方法去避免这些小问题 1.[Header(" ")] 这个的作用是给它下面的所有变量一个总标题 2.[Tooltip("")] 这个的作用是给下面的第一行(紧邻的语句)注释 这个注释和双斜杠的注释不同 这个注释的效果是在unity中鼠标拖到变量的名字上 他会出现注释(括号的双引号中的

随机推荐