Unity3D自定义资源配置文件

时间:2022-02-15 12:39:53

http://blog.csdn.net/candycat1992/article/details/52181814

写在前面

我竟然最近两天才知道Unity中ScriptableObject的存在……

看了下ScriptableObject的一些介绍,最大的优点感觉有三点:

  • 把数据真正存储在了资源文件中,可以像其他资源那样管理它,例如退出运行也一样会保持修改
  • 可以在项目之间很好的复用,不用再制作Prefab那样导入导出
  • 在概念上有很好的fit,强迫症患者的福音

看了下感觉有很多东西都可以用它。之前的做法一般都是[Serializable]一个class,然后在面板里配数据,做成prefab,但这种方法没有上面的三个优点。感觉今后如果有类似通过Serializable + Class + Prefab实现存储数据的想法的时候,都应该先考虑下能不能用ScriptableObject做成一个真正的资源文件。

当然了,ScriptableObject还有很多可以应用的地方,例如跟多态结合起来可以做各种特效、音效、技能、对话的资源配置文件。当然不用ScriptableObject也是可以完成这些需求的,感觉ScriptableObject提供了一种更优雅的实现方法。

下面的内容主要参考以下资料:

MonoBehaviour Tyranny

Unity3D自定义资源配置文件

为什么某些情况下使用MonoBehaviour很不好:

  • 运行时刻修改了数据一退出就全部丢失了。
    • 这个深有感触,目前都是靠Copy Component Values来解决,很麻烦。其实有这样的需求的时候大部分就说明这个脚本存储的是很多数据,就应该考虑使用ScriptableObject,而不是MonoBehaviour。说到底是因为这些对象不是Assets
  • 当实例化新的对象的时候,这个MonoBehaviour也在内存中多了一份实例,浪费空间
  • 在场景和项目之间很难共享
  • 在概念上就很难定义这种对象,明明是为了存储一些数据和设置等,但却要作为一个Component附着在Gameobject或Prefab上,不能独立存在

为什么使用C#的statics也无法解决这个问题:

  • 一旦退出运行仍然会重置所有数据
  • 需要自己进行serialsation,而且不容易引用其他Unity对象(因为有Static限制)
  • 显示面板也需要我们自己实现,很麻烦

为什么Prefabs也不行:

  • Prefab的确可以在项目和场景之间贡献,但很容易被搞得乱七八糟,我们只需要实例化一个prefab,然后就可以随意更改数据了
  • 会有额外的一些Component,但其实我们只是想要存储数据而已,这些没有任何意义
  • 仍然在概念上不能更好的fit

ScriptableObject是我们的rescue!

  • 在内部实现上它仍然继承自MonoBehaviour,但它不必附着在某个对象上作为一个Component
  • 我们也不能(当然初衷就是不愿意)把它赋给Gameobject或Prefab
  • 可以被serialised,而且可以自动有类似MonoBehavior的面板,很方便
  • 可以被放到.asset文件中,也就是说我们可以自定义asset的类型。Unity内置的asset资源有材质、贴图、音频等等,现在依靠ScriptableObject我们可以自定义新的资源类型,来存储我们自己的数据
  • 可以解决某些多态问题

Unity3D自定义资源配置文件

ScriptableObject是如何解决我们的问题的:

  • ScriptableObject的数据是存储在asset里的,因此它不会在退出时被重置数据,这类似Unity里面的材质和纹理资源数据,我们在运行时刻改变它们的数值就是真的改变了
  • 这些资源在实例化的时候是可以被引用,而非复制
  • 类似其他资源,它可以被任何场景引用,即场景间共享
  • 在项目之间共享
  • 没有其他多余的东西,例如多余的Component

当然ScriptableObject也有一些缺点:

  • 很少的回调函数
    • OnEnable
    • OnDisable
    • OnDestroy
  • 真正意义上的共享,因此一旦修改数据都真的修改了

总结:其实说明白点,ScriptableObject的优点和缺点都是因为它表现起来就像一个类似材质、纹理等类型的资源,存在于Assets文件夹下,只有唯一实例

如何使用

非常简单,只需要把平时的继承自MonoBehaviour改成ScriptableObject即可:

基本使用

using UnityEngine;

[CreateAssetMenu(menuName="MySubMenue/Create MyScriptableObject ")]
public class MyScriptableObject : ScriptableObject
{
public int someVariable;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

其中,CreateAssetMenu可以让我们在资源创建菜单中添加创建这个ScriptableObject的选项,类似创建脚本、材质等其他资源。

我们也可以在脚本中动态创建一个ScriptableObject:

ScriptableObject.CreateInstance<MyScriptableObject >();
  • 1

这会在内存中创建一个新的实例,用作临时修改等用途,然后在不使用的时候可以让GC回收。

回调函数

Unity3D自定义资源配置文件

create可以是从脚本中被创建,当有其他对象引用该ScriptableObject时它会被load。

生命周期

Unity3D自定义资源配置文件

其实ScriptableObject的生命周期和其他资源都是类似的:

  • 当它是被绑定到.asset文件或者AssetBundle等资源文件中的时候,它就是persistent的,这意味着
    • 它可以通过Resources.UnloadUnusedAssets来被unload出内存
    • 可以通过脚本引用或其他需要的时候被再次load到内存
  • 如果是通过CreateInstance<>来创建的,它就是非persistent的,这意味着 
    • 它可以通过GC被直接destroy掉(如果没有任何引用的话)
    • 如果不想被GC的话,可以使用HideFlags.HideAndDontSave

什么时候使用

下面介绍一些常见的应用场景。

Data Objects和Tables

第一种最常见的就是数据对象和表格数据,我们可以在Assets下创建一个.asset文件,并在编辑器面板中编辑修改它,再提交这个唯一的一份资源给版本控制器。例如,本地化数据、清单目录、表格、敌人配置等(这些真的非常常见,目前我接触过的大部分都是通过json、xml文件或是Monobehaviour来实现的,json和xml文件对策划并不友好,Monobehaviour的问题前面就说过了)。

一个例子:

class EnemyInfo : ScriptableObject {
public int MaximumHealth;
public int DamagePerMeleeHit;
}
  • 1
  • 2
  • 3
  • 4

记住,ScriptableObject的目的是只有一份,因此这里面不应该包括一些会根据实例不同而变化的数值。例如,我们在这个例子里没有声明敌人的生命值等变量,这是因为不同的敌人的生命值可能是不同的,这些属性应该在相应的MonoBehaviour里定义。

然后,我们就可以在真正的MonoBehaviour脚本中声明对ScriptableObject的引用:

class Enemy : MonoBehaviour {
public EnemyInfo info;
}
  • 1
  • 2
  • 3

这保证所有的Enemy都会引用到同一个ScriptableObject对象。

Dual Serialisation

使用ScriptableObject的一个好处是你不需要考虑序列化的问题,但是我们也可以和Json这些进行配合(使用JsonUtility),既支持直接在编辑器里创建ScriptableObject,也支持在运行时刻通过读取Json文件来创建。例子是,内置 + 用户自定义的场景文件,我们可以在编辑器里设计一些场景存储成.asset文件,而在运行时刻玩家可以自己设计关卡存储在Json文件里,然后可以据此生成相应的ScriptableObject。

一个例子:

[CreateAssetMenu]
class LevelData : ScriptableObject { ... } LevelData LoadLevelFromFile(string path) {
string json = File.ReadAllText(path);
LevelData result = CreateInstance<LevelData>();
JsonUtility.FromJsonOverwrite(result, json);
return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

JsonUtility.FromJsonOverwrite会使用Json文件中的数据来更新LevelData。

Reload-proof Singleton

我们经常会需要一个可以在场景间共享的Singleton对象,有时候我们就可以使用ScriptableObject + static instance variable的方法来解决,当场景变换的时候,我们可以使用Resources.FindObjectsOfTypeAll<>来找到已有的instance(当然这需要在实例化第一个instance的时候把它标识为instance.hideFlags = HideFlags.HideAndDontSave)。一个例子就是游戏状态和游戏设置。

一个例子:

class GameState : ScriptableObject {
public int lives, score;
private static GameState _instance;
public static GameState Instance {
get {
if (!_instance) {
// 如果为空,先试着从Resource中找到该对象
_instance = Resources.FindObjectOfType<GameState>();
}
if (!_instance) {
// 如果仍然没有,就从默认状态中创建一个新的
// CreateDefaultGameState函数可以是从JSON文件中读取,并且在实例化完后指明_instance.hideFlags = HideFlags.HideAndDontSave
_instance = CreateDefaultGameState();
}
return _instance;
}
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

Delegate Objects

ScriptableObject除了可以存储数据外,我们还可以在ScriptableObject中定义一些方法,MonoBehaviour会把自身传递给ScriptableObject中的方法,然后ScriptableObject再进行一些工作。这类似于插槽设计模式,ScriptableObject提供一些槽,MonoBehaviour可以把自己插进去。适用于AI类型、加能量的buff或debuffs等

这种用法大概是最常见的。首先看一个加能量的例子(来源Unite 2016 Europe)。

一个例子:

abstract class PowerupEffect : ScriptableObject {
public abstract void ApplyTo(GameObject go);
} [CreateAssetMenu]
class HealthBooster : PowerupEffect {
public int Amount;
public override void ApplyTo(GameObject go) {
go.GetComponent<Health>().currentValue += Amount;
}
} class Powerup : MonoBehaviour {
public PowerupEffect effect;
public void OnTriggerEnter(Collider other) {
effect.ApplyTo(other.gameObject);
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

我们先声明了一个PowerupEffect抽象类,来规定所有的加能量技能都需要定义一个ApplyTo函数作用于玩家。然后,我们定义一个HealthBooster类来管理那些专门加血的技能,我们可以通过创建资源的方式创建多个加血技能的资源实例,它们每个都可以有不同的加血量(Amount),当传进来一个GameObject的时候,就可以给它加血。我们又定义了Powerup的MonoBehaviour类,把它作为Component赋给各个可以触发加血技能的物体,它们可以接受一个PowerupEffect类型的加能量技能,然后靠碰撞体触发加血行为。

Tank Demo

在参考资料中的Tank Demo中有更多的例子。

Delegate Objects

这种是非常常见的一种ScriptableObject应用模式。

例子:音效事件资源

首先是播放音效被定义成一个ScriptableObject资源对象。

public abstract class AudioEvent : ScriptableObject
{
public abstract void Play(AudioSource source);
}
  • 1
  • 2
  • 3
  • 4

上面的AudioEvent可以用于定义一个播放音效的事件资源。所有继承它的类都需要定义播放Play函数,以便其他MonoBehaviour在运行时刻可以传递一个AudioSource文件给它进行播放。

一个简单的例子是:

[CreateAssetMenu(menuName="Audio Events/Simple")]
public class SimpleAudioEvent : AudioEvent
{
public AudioClip[] clips; public RangedFloat volume; [MinMaxRange(0, 2)]
public RangedFloat pitch; public override void Play(AudioSource source)
{
if (clips.Length == 0) return; source.clip = clips[Random.Range(0, clips.Length)];
source.volume = Random.Range(volume.minValue, volume.maxValue);
source.pitch = Random.Range(pitch.minValue, pitch.maxValue);
source.Play();
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

SimpleAudioEvent可以管理一个音效列表,然后在播放时随机选取一个进行播放。RangedFloat和MinMaxRanges也是自定义的类型,同时我们也为SimpleAudioEvent和RangedFloat定义了面板显示编辑器类AudioEventEditor.cs和RangedFloatDrawer.cs,不再赘述。最终,我们可以通过创建资源菜单来创建一些真正的音效事件资源:

Unity3D自定义资源配置文件

上面一共显示了4个音效播放资源,我们选中的是一个庆祝时会播放的音频事件Celebration,它会随机播放一个大笑的音效。我们可以点击Preview按钮来预览播放效果,这个也是在编辑器类AudioEventEditor.cs中定义的。

最后,我们可以把这些资源拖拽给需要播放音效的MonoBehaviour,例如子弹爆炸的脚本:

Unity3D自定义资源配置文件

例子:AI

Tank游戏里面的坦克可以是由不同的AI控制的,一种是由玩家自己控制,一种是由电脑扮演,这种也可以有不同的行为。我们可以把控制坦克行为的brain也定义成一种ScriptableObject资源:

public abstract class TankBrain : ScriptableObject
{
public virtual void Initialize(TankThinker tank) { }
public abstract void Think(TankThinker tank);
}
  • 1
  • 2
  • 3
  • 4
  • 5

TankBrain必须实现两个方法,一个是根据输入的坦克实体TankThinker(是一个MonoBehaviour类) 初始化自己,一个是根据TankThinker进行Think行为的函数。

然后,我们可以定义玩家控制类PlayerControlledTank:

[CreateAssetMenu(menuName="Brains/Player Controlled")]
public class PlayerControlledTank : TankBrain
{ public int PlayerNumber;
private string m_MovementAxisName;
private string m_TurnAxisName;
private string m_FireButton; public void OnEnable()
{
m_MovementAxisName = "Vertical" + PlayerNumber;
m_TurnAxisName = "Horizontal" + PlayerNumber;
m_FireButton = "Fire" + PlayerNumber;
} public override void Think(TankThinker tank)
{
var movement = tank.GetComponent<TankMovement>(); movement.Steer(Input.GetAxis(m_MovementAxisName), Input.GetAxis(m_TurnAxisName)); var shooting = tank.GetComponent<TankShooting>(); if (Input.GetButton(m_FireButton))
shooting.BeginChargingShot();
else
shooting.FireChargedShot();
}
}
  • 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

类似的还可以定义直接由电脑控制的其他AI。在Think函数里我们可以进行和实现MonoBehaviour方法类似的功能,例如通过GetComponent、FindGameobject等函数来获取游戏对象。

然后,我们就可以在创建真正的Brain资源:

Unity3D自定义资源配置文件

上面显示了一个Idiot类型AI的Brain资源。

然后,我们可以在游戏管理类GameManager里面定义两个玩家,并把需要的Tank Brain资源拖拽进去:

Unity3D自定义资源配置文件

Reload-proof Singleton

例子:游戏设置

这个例子是说用户可以在开始菜单里定义Tank Brain的个数和类型,然后这个设置(GameSettings)会作为一个Singleton传递给后面的关卡中。GameSettings类我们很多时候其实都是直接用普通的Singleton类(值得说明的是Unity里面实现的Singleton类通常都是靠DonotDestroyOnLoad等“费劲的方法”来实现)来做的,它会在开始的时候读取Json文件,在必要的时候再写回Json进行保存。这里选择在ScriptableObject + Singleton的方法来实现的一个好处是我们不需要什么其他繁冗的步骤,就可以保证唯一性和在场景之间共享的目的,因为ScriptableObject本身可以认为是一种资源:

[CreateAssetMenu]
public class GameSettings : ScriptableObject
{
[Serializable]
public class PlayerInfo
{
public string Name;
public Color Color; ...
} public List<PlayerInfo> players; private static GameSettings _instance;
public static GameSettings Instance
{
get
{
if (!_instance)
_instance = Resources.FindObjectsOfTypeAll<GameSettings>().FirstOrDefault();
#if UNITY_EDITOR
if (!_instance)
InitializeFromDefault(UnityEditor.AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Test game settings.asset"));
#endif
return _instance;
}
} public int NumberOfRounds; public static void LoadFromJSON(string path)
{
if (!_instance) DestroyImmediate(_instance);
_instance = ScriptableObject.CreateInstance<GameSettings>();
JsonUtility.FromJsonOverwrite(System.IO.File.ReadAllText(path), _instance);
_instance.hideFlags = HideFlags.HideAndDontSave;
} public void SaveToJSON(string path)
{
Debug.LogFormat("Saving game settings to {0}", path);
System.IO.File.WriteAllText(path, JsonUtility.ToJson(this, true));
} public static void InitializeFromDefault(GameSettings settings)
{
if (_instance) DestroyImmediate(_instance);
_instance = Instantiate(settings);
_instance.hideFlags = HideFlags.HideAndDontSave;
} #if UNITY_EDITOR
[UnityEditor.MenuItem("Window/Game Settings")]
public static void ShowGameSettings()
{
UnityEditor.Selection.activeObject = Instance;
}
#endif ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

GameSettings继承了ScriptableObject,并支持我们在资源文件夹中创建一个资源文件,这个资源文件可以是在游戏第一次运行时候的一个默认的玩家配置。然后,它实现了LoadFromJSON和SaveToJSON函数来加载和存储硬盘上的数据。一个有趣的地方是上面的最后一个函数,这个函数允许我们在菜单栏上打开并显示当前的GameSettings资源,非常方便,不需要再自己写窗口类了。

在欢迎界面上,我们可以在控制类MainMenuController中获取GameSettings,并在进入下一关前保存数据到硬盘:

public class MainMenuController : MonoBehaviour
{
public GameSettings GameSettingsTemplate; ... public string SavedSettingsPath {
get {
return System.IO.Path.Combine(Application.persistentDataPath, "tanks-settings.json");
}
} void Start () {
if (System.IO.File.Exists(SavedSettingsPath))
GameSettings.LoadFromJSON(SavedSettingsPath);
else
GameSettings.InitializeFromDefault(GameSettingsTemplate); foreach(var info in GetComponentsInChildren<PlayerInfoController>())
info.Refresh(); NumberOfRoundsSlider.value = GameSettings.Instance.NumberOfRounds;
} public void Play()
{
GameSettings.Instance.SaveToJSON(SavedSettingsPath);
GameState.CreateFromSettings(GameSettings.Instance);
SceneManager.LoadScene(1, LoadSceneMode.Single);
} ...
}
  • 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

在之后的场景中,我们只需要GameSettings.Instance来访问就可以了。

Unity3D自定义资源配置文件的更多相关文章

  1. 专业版Unity技巧分享:使用定制资源配置文件

    http://unity3d.9tech.cn/news/2014/0116/39639.html 通常,在游戏的开发过程中,最终会建立起一些组件,通过某种形式的配置文件接收一些数据.这些可能是程序级 ...

  2. 专业版Unity技巧分享:使用定制资源配置文件 ScriptableObject

    http://unity3d.9tech.cn/news/2014/0116/39639.html 通常,在游戏的开发过程中,最终会建立起一些组件,通过某种形式的配置文件接收一些数据.这些可能是程序级 ...

  3. 在VC项目中使用自定义资源

    这是我看到的比较实用性的技巧,分享给大家 一.前言 在VC环境中除了我们所常用的Dialog.Menu和Bitmap等标准资源类型之外,它还支持自定义资源类型(Custom Resource),我们自 ...

  4. Unity3d Web3d资源的动态加载

    Unity3d Web3d资源的动态加载 @灰太龙 参考了宣雨松的博客,原文出处http://www.xuanyusong.com/archives/2405,如果涉及到侵权,请通知我! Unity3 ...

  5. SpringBoot学习&lpar;三&rpar;--&gt&semi;Spring的Java配置方式之读取外部的资源配置文件并配置数据库连接池

    三.读取外部的资源配置文件并配置数据库连接池 1.读取外部的资源配置文件 通过@PropertySource可以指定读取的配置文件,通过@Value注解获取值,具体用法: @Configuration ...

  6. Spring读取外部的资源配置文件—&commat;PropertySource和&commat;Value实现资源文件配置

    通过@PropertySource可以指定读取的配置文件,通过@Value注解获取值: @PropertySource注解主要是让Spring的Environment接口读取属性配置文件用的,标识在@ ...

  7. 自定义vim配置文件vimrc,用于c&sol;c&plus;&plus;编程

    vim作为Linux下广受赞誉的代码编辑器,其独特的纯命令行操作模式可以很大程度上方便编程工作,通过自定义vim配置文件可以实现对vim功能的个性化设置. vim配置文件一般有两份,属于root的/e ...

  8. 【spring boot logback】日志颜色渲染,使用logback-spring&period;xml自定义的配置文件后,日志没有颜色了

    接着spring boot日志logback解析之后,发现使用logback-spring.xml自定义的配置文件后,日志没有颜色了 怎么办? 官网处理日志链接:https://logback.qos ...

  9. Struts2 资源配置文件国际化

    Struts2 资源配置文件国际化 Struts2资源文件的命名规范:basename_language_country.properties Struts2国际化如果系统同时存在资源文件.类文件,系 ...

随机推荐

  1. 记一次在*上问问题的经历

    最近一直在做测试方面的事情,被测的一些功能需要连接到FTP服务器上.而我在做本地测试时为了方便,就使用java写了一个简单的ftp服务器,可以在命令行下直接启动运行. 当时在main函数里是这样写的. ...

  2. 快速排序模板qsort(转载)

     qsort  用 法: void qsort(void *base, int nelem, int width, int (*fcmp)(const void *,const void *)); 各 ...

  3. Spring的DI&lpar;Ioc&rpar; - 注入bean 和 基本数据类型

    注入bean有两种方式: 注入其他bean: 方式一 <bean id="orderDao" class="cn.itcast.service.OrderDaoBe ...

  4. Berkeley DB数据处理

    设计一个结构,利用Berkeley DB完成大数据的存储,备份,查询功能. 已有的储备: 1.Berkeley DB的基本操作. 2.数据转存后数据不丢失. 3.过百GB以上数据的存储. 数据流如下, ...

  5. UML中常见关系详解(泛化、实现、依赖、关联、组合、聚合)

    UML中类与类,已经类与接口,接口与接口的关系有:泛化(generalization),关联(association),依赖(dependency),实现(realization)这几种.   泛化( ...

  6. 备战&OpenCurlyDoubleQuote;软考”之软件project

    说到"软件project"就有一种非常纠结的感觉!为什么呢?由于刚进入软考复习阶段,大家都把它放到了"比較"次要的地位,由于已经学过两遍,再加上它没有非常难理解 ...

  7. 使用NODEJS&plus;REDIS开发一个消息队列以及定时任务处理

    作者:RobanLee 原创文章,转载请注明: 萝卜李 http://www.robanlee.com 源码在这里: https://github.com/robanlee123/RobCron 时间 ...

  8. UVA - 11292 Dragon of Loowater 贪心

    贪心策略:一个直径为X的头颅,应该让雇佣费用满足大于等于X且最小的骑士来砍掉,这样才能使得花费最少. AC代码 #include <cstdio> #include <cmath&g ...

  9. react native进一步学习(NavigatorIOS 学习)

    特别申明:本人代码不作为任何商业的用途,只是个人学习的一些心得,为了使得后来的更多的程序员少走一些弯路.*(如若侵犯你的版权还望见谅)*. 开发工具:WebStorm,xcode 1. rn的创建的时 ...

  10. samba服务配置(一)

    samba是一个实现不同操作系统之间文件共享和打印机共享的一种SMB协议的免费软件. samba软件结构: /etc/samba/smb.conf    #samba服务的主要配置文件 /etc/sa ...