【Unity】Unity单例模式
在unity中,经常需要使用到单例,尤其是MonoBehaviour的单例。
MonoBehaviour的单例模式需要使用饿汉模式的单例来初始化,要不然可能会自动创建多出的单例。
MonoBehaviour单例
1 | using UnityEngine; |
非MonoBehaviour单例
1 | /// <summary> |
在unity中,经常需要使用到单例,尤其是MonoBehaviour的单例。
MonoBehaviour的单例模式需要使用饿汉模式的单例来初始化,要不然可能会自动创建多出的单例。
1 | using UnityEngine; |
1 | /// <summary> |

Changes to be committed
文件已经被git add <file>存放在暂存区,现在有两条路可以走
git commit -m ""提交git restore --staged <file>或git reset <file>将文件复原到工作区,文件修改的内容会保留Changes not staged for commit
文件被修改,但是还为被git add <file>
git add <file>git restore <file>或git checkout <file>将文件的内容恢复成上一次提交的内容Untracked files
新建的文件,还未被跟踪。这种状态的文件可以使用.gitignore文件取消跟踪
git add <file>跟踪文件.gitignore屏蔽文件1 | // 查看绑定了哪些远程仓库 |
1 | # 将本地的<branch_name>分支推送到远程的<branch_name>分支上 |
还可以添加-u参数设置默认的推送和拉取映射关系
使用这个参数之后可以直接使用git push和git pull而不用再添加参数
1 | # 将本地的master分支推送到origin的master分支上,并设置这个为默认的推送和拉取映射 |
注意:
经过测试发现携带-u参数时使用master:main的映射方法,后续使用git push无法成功的将本地的master分支推送到远程的main上
也就是说,如果使用了-u参数,那么需要推送到的远程分支名必须与本地分支名一致
有两种方法git pull和git fetch
两者的关系是:git pull是执行了git fetch和git merge两个操作
1 | # 将远程的<branch_name>分支拉取到本地的<branch_name>分支上 |
可直接使用git pull <origin_name> <branch_name>将<origin_name>/<branch_name>的内容直接合并到本地
1 | git fetch <origin_name> <branch_name> |
git fetch <origin_name> <branch_name>将<origin_name>/<branch_name>的内容保存到.git/FETCH_HEAD文件中git merge FETCH_HEAD将FETCH_HEAD中的信息合并到当前分支git checkout -b <new_local_branch> <origin/remote_banch_name>
在本地创建一个<new_local_branch>分支并将<origin/remote_banch_name>
一般要使用这个命令是因为遇到了冲突,详细处理方法可以查看另一篇文章【Git】合并分支及如何解决冲突
1 | # 查看所有分支的提交记录之间的关系(以一行显示) |
1 | git count-objects -vH |
在使用这个命令之前可以先使用git gc优化一下仓库。可以看到size-pack参数就是仓库的大小
这个命令是获取当前仓库文件的大小的,与你当前本地文件的大小不一样
仓库文件:只要上传过的文件,就会计入在里面本地文件:使用window的资源管理器,所见即所得,没有其他的文件一个特殊的情况是:
你不小心上传了一个1G的文件夹,现在你想要把他排除在外,使用
git rm --cached logs/成功排除了这个文件夹,并且以后也都不会再跟踪这个文件夹了。但是你在这之前上次的1G大小的文件依然是还存在这个git仓库里的,并不会消失,使用上面的这个命令获取到的文件大小是包含这个
logs/文件的目前没有找到彻底的删除这个1G的文件,然后你又不能回退版本的话,现在只能将你仓库根目录下的
.git文件删除掉,再从新git init初始化一个仓库了。当然在做这个操作之前,建议先备份一下
1 | git ls-files |
这个命令有很多参数,具体可以使用git ls-files --help打开本地的帮助文档查看
1 | git ls-files -z | xargs -0 du -hc | grep total$ |
可以列出所有文件大小。
-z:\0行终止输出,不引用文件名的输出格式,具体可看本地帮助文档-0:防止文件中有空格-h:以人类可读的格式显示文件和目录的大小-c:在最后一行显示总大小,包括所有指定目录的大小total$:正则匹配
在现有的仓库中创建一个全新的、完全空的分支
git checkout --orphan <branch_name>
在linxu中每次都要输入账号密码,使用此命令后,push的时候再输入一次密码就不用再输入了
1 | git config --global credential.helper store |
默认情况下git中文显示为ASCII码,使用该命令之后能正常显示中文
1 | git config --global core.quotepath false |
这个警告提示的意思是,Git 在下次处理文件(例如提交或检出)时,会将文件中的换行符从 CRLF转换为 LF
Git 对不同操作系统的换行符处理有不同的默认设置。通常,Git 会尝试在不同操作系统之间处理换行符的差异。例如:
保持默认行为: 如果你不介意这种转换行为,可以忽略这个警告。Git 会在提交时自动处理换行符。
设置 Git 配置: 你可以通过全局 Git 配置或者设置 .gitattributes 文件来控制换行符的处理方式。
全局Git配置:在git shell上执行(使用了这个就不用配置.gitattributes文件了)
1 | git config --global core.autocrlf true # Windows 用户推荐 |
配置 .gitattributes 文件: 在仓库根目录下创建或编辑 .gitattributes 文件,添加以下规则来强制 Git 使用一致的换行符:(这个配置会覆盖全局设置)
1 | # 强制所有文本文件使用 CRLF 作为换行符 |
1 | sed -i 's/\r$//' path/to/file.text |
如果之前已经提交过文件了,可以按照以下步骤重新设置文件的换行符
配置.gitattributes文件
1 | # 强制所有文本文件使用 LF 作为换行符 |
删除Git缓存文件
1 | git rm --cached -r . |
这个命令会将所有已跟踪的文件从 Git 的暂存区中移除,但不会删除工作目录中的文件。此时,Git 会认为这些文件已被删除。
重新添加所有文件到暂存区并提交更改
1 | git add . # 此时可能依然还会报警告,但是不要紧。我们只要存储一次,修改文件在git中的规则之后,下次就不会有这个警告了 |
此时,Git 会根据 .gitattributes 文件中指定的换行符规则重新处理这些文件,并将它们添加回暂存区。
验证更改
1 | git ls-files --eol |
如果输出的内容是,以下内容就没问题
1 | i/lf w/crlf attr/text=auto eol=crlf .gitattributes |
i/lf:当前工作副本中的换行符类型因为使用的是git bash,是Unix,所以工作副本里是lfw/crlf: Git 将在下次检出(checkout)或其他操作时将会使用的换行符类型attr/text=auto eol=crlf:这是 .gitattributes 文件中的设置也可以将文件使用NotePad++或rider等编辑器打开,查看右下角的编码格式
经过测试发现,在创建Unity的C#脚本的时候.cs文件默认是crlf,.meta文件使用的lf。
所以只要你启用了Git全局配置的自动转换,或则在.gitattributes中指定了规则,你都无法完全避免这个警告。
唯一能完全取消这个警告的方法是:不再规范所有文件的换行规则
具体的做法就是:
git config --global core.autocrlf false:取消自动转换.gitattributes注释掉换行规则这样才能完全让这个警告消失在你的视野中
但正如“有得便有失”,你眼睛不会被脏了,但是代价是什么呢?
注意:
不再规范所有文件的换行规则后
- 跨平台使用仓库,会影响文件的内容,从而影响版本控制和合并
- 多人员开发时,没有规范换行规则也会影响文件内容,从而影响版本控制
所以只有在个人独自开发,且没有多平台需求的开发项目才能完全避免这个警告。
2024.10.9日更正:
可以更改unity的换行符格式
Project Setting -> Editor -> Asset Serialization -> Mode
Mixed:混合模式Force Binary:.meta等文件将使用CRLF换行符格式Force Text:meta等文件将使用LF换行符格式
标题虽然是小技巧,但本篇文章里还是记录了一些常用的功能,本文章的主要目的是方便日后快速查找
时间复杂度为O(nlogn)
默认排序
1 | int[] numbers = { 5, 2, 8, 3, 1 }; |
自定义比较器
1 | string[] names = { "Alice", "Bob", "Charlie" }; |
多条件排序
1 | class Person |
部分排序
1 | List<int> numbers = new List<int> { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 }; |
拓展:
1 public delegate int Comparison<in T>(T x, T y);输入参数:
x和y是要比较的两个对象,类型为T。返回值:
- 一个整数,用来表示排序顺序:
- 负数:表示
x小于y(x排在y前面)。- 零:表示
x等于y(两者顺序不变)。- 正数:表示
x大于y(x排在y后面)。
1 x.CompareTo(y) = 20.CompareTo(10) // 返回1
1
2
3
4
5
6 List<int> numbers = new List<int> { 5, 2, 8, 3, 1 };
numbers.Sort((x, y) => x.CompareTo(y)); // 使用 Lambda 表达式定义 Comparison<T>
// 输出:1, 2, 3, 5, 8
numbers.ForEach(Console.WriteLine);
nameof():参数的名称(编译时确定好的常量),如果是泛型参数则输出Ttypeof().Name:类名的名称(编译时确定好的常量),如果是泛型参数则会输出泛型类实例化时的泛型类型(泛型函数的泛型参数的泛型参数)如果参数不是泛型参数,则与nameof()的效果一样obj.GetType().Name:实时获取obj的类型测试代码:
1 | public class Item{} |

1 | public class ChainedClass |
1 | ChainedClass cc = new ChainedClass("coffee") // 构造函数 |
1 | public static class DotsExtensions |
重写+、-、*、/、|、$、!=、==,完全由自己定义,返回的类型甚至不需要是自身
1 | public struct CustomStruct // 结构体或类都可以 |
可以像数字一样直接使用+法了
1 | CustomStruct cs1 = new CustomStruct() { X = 1, Y = 2, Z = 3 }; |
1 | public struct PlayerMoveInput |
1 | var input = new PlayerMoveInput(); |
和隐式转换的语法一样,将implicit换成explicit就OK了
1 | public class PlayerMoveInput |
1 | var input = new PlayerMoveInput(); |
单例类,是不需要外界实例化的,可以将构造函数私有化,外界就无法实例化了。
1 | class Manager |
不希望外界访问这个接口里所有的方法时,可以显示实现接口
1 | public interface IInterface |
1 | class ReadOnlyClass : IInterface // 创建一个只能读取,不能更改内容的类 |
ReadOnlyDictionary<TKey, TValue>()、ReadOnlyList<>、ReadOnlyCollection<>、ReadOnlySet<>都是使用显示实现接口的方法隐藏了修改的方法,使外界只能读取,不能修改
完整使用方法:
1 | class Manager |
拓展:
{ get; private set; }:在类内部还能重新setter
{ get; }:只能在构造函数中setter
创建一个按照特定规则排列的列表:
例如,实例化一些Human,并且将他们按照age从大到小的方式放置一个列表中
1 | public interface IAnimal |
定义一个单例泛型类,用来做比较
1 | public class ReverseComparer<T> : IComparer<T> |
1 | public class HumanSelector : SortedList<int, IAnimal> // 继承`SortedList<int, IAnimal>`基类 |
使用:
1 | Human A = new Human(20, "A"); |
这样,我们就将排序封装起来了,使用者只需要使用Add将元素添加到排序列表中就OK了
最快速也是最简单的方法:反向遍历,从大索引号开始遍历
1 | List<int> list = new List<int>(){ 1, 2, 3, 4, 5 }; |
总得来说大致有三种用法
1 | private int _count; // 字段:内部使用 |
1 | private int _length; |
1 | public static int TotalCount { get; set; } // 属性:可读可写,这种方式实际上也隐式的创建了一个字段,如下图 |

ref修饰,使Run实际上是直接访问的_run这个字段,减少了一个复制的过程,提高性能1 | [SerializeField] |
1 | public string this[int index] |
甚至还可以将字典改成与python一样的用法:不过不推荐这种用法
1 | public class CustomizeDict |
default常用default能使代码排版更好看,可以省去写判断表达式
值类型
1 | int number = defaule; // 默认值为0 |
引用类型
1 | string text = default; // 默认值为null |
C#在8.0后字符串和数组可以像python一样使用切片,时代码变的即简单又美观
值得注意的是列表不能使用这个小技巧
示例:

1 | var str = @"HelloWorld"; |
^和python里的-一样:反着数,^1表示最后一个字符d,^5表示倒数第五个字符W这里面的所有简写rider都会提示,部分简写不建议使用,大大提高的代码的阅读效率
1 | var ran = new Random(); |
值得注意一点的是,这个判断的两个数必须是常量值
如果是
list.Count,这里就用不了了
为方便理解我先写成这样,解释一下每个方法的含义
1 | var list = new List<int>() { 5, 2, 7, 1, 9, 4 }; // 定义一个int列表 |
可简写成如下
1 | var list = new List<int>() { 5, 2, 7, 1, 9, 4 }; |
老实做法:
1
2
3
4
5
6 var list = new List<int>() { 5, 2, 7, 1, 9, 4 };
foreach (var i in list)
{
if (i > 3)
Console.WriteLine(i);
}这一样看上面的方法是不是即简洁又好看
但其实这些都是rider编辑器会提示或者直接帮我们转的,只要别说看不懂是什么意思就行了
A |= B:位或,左右两边的值进行位或,并将结果赋值给左边的变量。bool a |= 1 < 10; 结果为True。位或:A |= B,如果B是true,那么A就是true;否则A的值不变
result = A ?? B:如果A是null,返回B;否则返回A
result ??= new List<int>():如果result是空,就进行复制操作;否则不做任何操作
result = A is { age: 20 }:等价result = A != null && A.age == 20
虽然这个应该是很常用的,但还是提一下
有两种写法,其输出结果是一样的
1 | int count = 100; |
第二种方法的性能要比第一种好
但是第一种情况在一些特殊的情况下使用有奇效,比如多语言设置、字符串替换等功能
具体可以看C#中字符串类的一些实用技巧及新手常犯错误这个大佬的
以上用的算的比较多的,所以不是今天的重点,今天的重点在格式化插值和格式化输出
1 | double i = 32.53728012341241562; |
10表示占位10个位置,当位置不够的时候在前面用空格补充F4表示保留4位小数,F不区分大小写除了F外还有其他很多格式:
数字格式都不区分大小写
F:Fiexd-point格式。显示小数后几位。$"{1234567.89:F2}" => 1234567.89C:货币格式。显示货币符号和千位分隔符。$"{1234.56:c}" => ¥1,234.56E:科学计数法格式。显示非常大或非常小的数字。$"{1231421432:e3}" => 1.231e+009N:与F类似,但是N能显示整数上的千位分隔符。$"{21432.537280:n3}" => 21,432.537G:自动选择合适的格式。P:百分比格式。将数值乘以100,并在结果后面加上百分号。$"{0.12345:p2}" => 12.35%R:常规格式。不带任何格式的方式显示数字,但保留足够的精度(怎么保留没有仔细研究,简单测试了几下,没有找出规律)实用方法:
1 | float value = 546.5198; |
与数字格式不同,日期时间格式需要区分大小写
注意:码代码的时候一定要拼写正确,是
DateTime不是DataTime年月日的排布
DateTime会自动更具文化设置
先看看普通的格式长什么样DateTime.Now => 2024/8/11 4:50:47
d:短日期格式。$"{date:d}" => 2024/8/11
D:长日期格式。$"{date:D}" => 2024年8月11日
t:短时间格式。$"{date:t}" => 4:57
T:长时间格式。$"{date:T}" => 4:58:41
f:(短)完整日期和时间。$"{date:f}" => 2024年8月11日 4:59
F:(长)完整日期和时间。$"{date:F}" => 2024年8月11日 5:00:45
M或m:月日格式。$"{date:m}" => 8月11日
Y或y:年月格式。$"{date:m}" => 2024年8月
另外还有十分强大的自定义格式化
DateTime的格式化:
1 | DateTime date = DateTime.Now; |
输出:
1 | 2024年05月11日 05:05:09 上午 +08:00 |
TimeSpan的格式化:
1 | TimeSpan timeSpan = endTime - DateTime.Now; |
输出
1 | // 1天03时15分 |
使用Profiler.BeginSample(string)与Profiler.EndSample()方法捕获代码片段
1 | private void Update() |

在ugui中,给一个拥有Content Size Fitter的对象添加数个子节点后,该对象的大小并不一定会立刻改变成合适的大小,尤其是在该对象的父节点同样拥有Content Size Fitter组件的情况下。
解决方案:使用LayoutRebuilder.ForceRebuildLayoutImmediate(RectTransform)方法立刻刷新该对象的大小。
LayoutRebuilder.ForceRebuildLayoutImmediate(RectTransform)的作用
- 同步刷新布局:通常,Unity的UI布局(比如
VerticalLayoutGroup、HorizontalLayoutGroup等)在元素变动后,会在下一帧自动更新。如果你希望立刻看到变化效果,使用ForceRebuildLayoutImmediate可以强制马上重新布局。- 处理UI嵌套或动态变更:在有些场景下,通过代码动态增删控件、修改内容或尺寸,自动刷新不及时或不生效,此时调用本方法可确保UI排列正确。
拓展:
除了上面这个接口外,还有
LayoutRebuilder.MarkLayoutForRebuild(RectTransform)。
- Immediate:马上重新布局
- Mark:标记为需要重建,延迟到下一个布局更新阶段(效率更高)
一般可以两个都同时调用一下
Tweener和Sequence是Tween的子类Tweener是播放的单个动画Sequence控制Tweener的播放一个Tweener的循环为3时,那么他们三个算一个完整的动画
当Tweener添加到Sequence之后Tweener的大部分属性设置会被覆盖,比如循环,Pause()
最容易照成的误区,动画为从原点向前移动5,Restart()之后将重复刚才的动画,也就是说他会先回到原点再向前移动5
如果为设置SetAutoKill(false)会在动画播放完一次之后就销毁掉,并且使用_sequence.Restart()并不会有反应,如果动画不是第一次播放,并且没有效果的话,可以检查一下定义这个动画的时候有没有使用SetAutoKill(false)。但是在使用SetAutoKill(false)之后,记得在合适的时候使用_sequence = null销毁掉对象,避免内存泄漏
可以将其想象成视频的播放按钮,打开一个视频的时候他会开始自动播放,除非暂停。
暂停之后,需要点击Play继续播放
视频放完之后再次点击Play没用,只能Restart。
Tweener方法如下
Play():继续播放动画,在实例化Tweener的时候就会Pause():暂停动画,使用Play()继续播放PlayForward():动画向前播放PlayBackwards():动画向后播放Restart(bool, float):重新开始播放动画;是否忽略SequenceKill(bool):结束动画;如果为true就瞬间完成动画,false停在当前位置动画周期内触发的回调:
OnStart:只有在第一次播放时才会回调OnPlay:在动画开始播放时回调:第一次播放、Play()、暂停后的Play()、Restart()OnUpdate:动画播放的期间回调,最好不要嵌套再嵌套OnStepComplete:在每次完成一个循环时都会触发,以为着,如果loops为3将被调用3次,而OnComplete只在最后被回调一次OnComplete:在整个动画完成时调用,包括循环OnPause:在动画停止播放时回调,播放完整个动画、Pause()、Kill(true)需用户手动触发的回调:
OnKill:触发kill()时回调OnRewind:触发Restart()时回调OnWayPoineChange:唯一一个有参数的回调,主要用于DoPath函数,当走到一个点位时回调如果需要改变的是普通的数值,而不是Transform,可以使用一下方法
1 | private DOGetter<float> _radiusGetter; // 其实就是一个获取数值的委托 |
1 | // 可以让 m_tmp999 的文字从 currentView 在1.2秒内平滑的变成 realCoin |
1 | private Sequence bounceSequence; |
1 | private Tweener amplifyTween; |
当前dotween动画没播放完,便再次播放有冲突的操作,如连续多次播放、正播、倒播,导致显示不正常或报错。
解决方法:在每次开始执行播放动画时,先加上下面对应类似的杀死进程代码,就OK了
1 | transform.DOKill(); |
让DOTweenAnimation忽略Time.timeScale = 0的影响
1 | tween.SetUpdate(true); |
设置Rotate旋转模式
1 | transform.DOLocalRotate(new Vector3(0, 0, -360), 2, RotateMode.FastBeyond360) |
Form()DOTween的参数默认都是目标值,使用From后参数代表起点
1 | // 绝对位置,若当前坐标(1,0,0),即从5运动到1 |
SetLoops(int loops, LoopType loopType)loops:循环次数,-1为无数次循环loopType:循环模式Restart:从头开始循环(默认)Yoyo:交替来回移动Incremental:设置为相对运动后才生效,连续“向前”移动(A到B, B到B+(A-B), …)SetRelative()设置为相对运动
SetEase这里可以参考 👉 | https://easings.net | 👈 上的曲线效果







想要将DoTween转换成UniTask,需要在ProjectSetting - Player - OtherSetting - ScriptingDefineSymbols中添加UNITASK_DOTWEEN_SUPPORT
优点:
可以使用await,而不是OnComplete回调函数,使代码更直观
1 | private async void Start() |
可以使用WithCancellation方法取消DOTween
1 | public class UniTaskWithDOTween : MonoBehaviour |
注意:
如果想要重复使用tweenrs(
SetAutoKill(false)),则永远不会触发await下方的代码如果想要等待另一个时间点,可以使用扩展方法
AwaitForComplete,AwaitForPause,AwaitForPlay,AwaitForRewind,AwaitForStepComplete。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 public class UniTaskWithDOTween : MonoBehaviour
{
private Tweener tween;
private void Awake()
{
tween = transform.DOMove(new Vector3(5f, 0, 5f), 2).Pause();
}
private async void Start()
{
await tween.AwaitForPlay(); // 先是被挂起,当在Update中检测到鼠标右键点击后,再执行下方代码
Debug.Log("Play");
}
private void Update()
{
if (Mouse.current.rightButton.wasPressedThisFrame)
{
tween.Play();
}
}
}
参考
比较简单,看示例就能懂
自定义Editor还挺好用的,后面有机会补
总的来说大概有两种使用方法
有两种创建方式RegisterValidator、RegisterValidatorRule。这两个唯一不同的是,后者可以在Validator窗口上编辑是否禁用和配置其属性
检测全局的
比如说,相机必须要绑定角色,就可以通过这个方式来检测,具体代码如下
1 | #if UNITY_EDITOR |
将这个脚本放在项目中就可以了,不用做其他操作。然后Validator就会找寻场景中所有使用了PlayerCamera脚本的实例,检查player是否为空
如果没有绑定角色,就会出现下图Error

Root Object Validator非常适合验证Unity对象,例如ScriptableObject、Materials和Components。因为您只会收到对象本身的告警/错误消息,所以称为Root。
与Value Validator不同,在另一个对象中引用跟对象,不会引发额外的警告/错误
属于第一类,检测指定属性的
序列化用的,如果要实现存档,建议使用ES3
之前写了一篇介绍事件和委托的帖子,但是没怎么运用过,只是使用一些简单的事件
这次看了一个项目的源码,趁着这个机会好好研究一下事件和委托的作用
1 | public Action Action_A; |
Action:安全性较差,外部代码可以触发这个委托(使用.Invoke())event Action:安全性强,外部只能订阅或取消订阅(使用-=或+=),不能直接触发事件假设现在有两个类ClassA、ClassB,在以下情况推荐使用事件委托来实现
- 需要在特定的时机触发某个方法
- 需要使用其他实例的私有属性
- 某个方法的实现有两种限制,比如UI中需要该物体处于激活状态,且按下手柄的A键
前面两个都还比较好理解,第三种情况除了事件本身Invoke()调用的时机外,还可以通过注册(+=)和注销(-=)来控制这个事件中是否有任务来控制释放的时机
场景:
ClassA需要特定的时候(比如参数intA等于0、或某个按钮被按下)执行方法FuncA具体操作:
- 创建一个事件
ActionA- 初始化的时候注册这个事件
ActionA += FuncA- 当
intA等于0时触发这个事件ActionA.Invoke()
示例代码:
1 | public class ClassA : MonoBehaviour // 这里需要引用UniTask插件,为了简洁我就不写了引用了 |
场景:
ClassA需要特定的时候(比如参数intA等于0、或某个按钮被按下)获取其他类中的私有方法FuncB或参数strB具体操作:
ClassA创建一个事件ActionA在
ClassB中注册这个事件ClassA.ActionA += FuncB当
intA等于0时ActionA.Invoke()
示例代码:
当ClassA中的intA等于0时,就会执行ClassB中的FuncB了
1 | public class ClassA : MonoBehaviour |
1 | public class ClassB : MonoBehaviour |
场景:
ClassA需要特定的时候(比如参数intA等于0、某个按钮被按下),且B处于激活状态时执行方法FuncA具体操作:
ClassA创建事件ActionA- 在激活
B时将FuncA注册给ActionA,停用B时将FuncA注销掉
示例代码:
1 | public class ClassA : MonoBehaviour |
1 | public class ClassB : MonoBehaviour |
为了方便观看将3DRPG游戏部署到了网站上https://www.coffeeofnosugar.top/unitygame/3drpgv3/
由于头发的物理模拟并不支持WebGL,所以头发看起来像塑料一样
使用UI Builder从零制作怪物行为树可视化编辑器,通过视图连接各个节点,实现定制化怪物AI。
CharacterController控制玩家移动Input System监听玩家输入Animator控制玩家动画,下一步计划将舍弃Animator转而使用Playable播放动画1 | nginx -t # 检测配置文件 |
配置文件为:./nginx/conf/nginx.conf
静态文件为:./nginx/html/
location [ = | ~ | ~* | ^~ ] uri { ... },其中|表示你可能会用到的语法
=:精确匹配~:区分大小写的正则匹配~*:不区分大小写的正则匹配^~:uri以某个字符串开头1 | local ^~ /unitygame/ { |
/unitygame/3drpg:通用匹配/:默认匹配优先级:= > ^~ > ~ > ~* > 最长的通用匹配 > 默认匹配
root会与URI的剩余部分(例子中为”/i/“)拼接,而alias不会
1 | localtion /i/ { |
这里贴一个我自己的完整配置
配置一:
1 |
|
配置二:
注意:
- 如果你当前的用户不是root时启动nginx,那么nginx的主进程(master process)是root用户,而工作进程会是nobody,而nobody不一定有html文件的读取权限,所以需要设置user为你当前登入的用户
- root的路径必须是要是全路径
- 如果还是连接不到网站,可以查看error.log日志,有详细的错误信息
1 |
|
创建文件/etc/systemd/system/qsign.service,注意Service的参数需要使用绝对路径
1 | [Unit] |
命令
| sudo systemctl daemon-reload | 重载服务,每次修改都要 |
|---|---|
| netstat -lntp | 查看端口情况 |
| sudo systemctl start qsign | 启动服务 |
| sudo systemctl stop qsign | 停止 |
| sudo systemctl restart qsign | 重启 |
| sudo systemctl enable qsign | 设置开机启动 |
| sudo systemctl disable qsign | 关闭开机启动 |
| sudo systemctl status qsign | 查看状态 |
| sudo journalctl -u qsign | 显示服务日志 |
| sudo journalctl -f | 持续输出所有后台服务器日志 |
如果screen内部运行的程序需要root权限,则也需要使用sudo操作下面的命令
| screen -S wardswar-fantasy | 创建会话 |
|---|---|
| screen -r {名称/id} | 重连会话 |
| screen -ls | 查看当前所有的会话 |
| screen -X -S {名称/id} quit | 关闭会话 |
| screen -dmS wardswar-fantasy bash -c “dotnet Main.dll -m Develop” | 直接创建一个在后台跑的任务 |
在会话内部快捷键
Ctrl+A,然后按D |
退出会话 |
|---|---|
Ctrl + A,然后按 C |
创建新窗口(可在一个会话中开多个”标签页”) |
Ctrl + A,然后按 N / P |
切换到下一个(N) / 上一个(P) 窗口 |
Ctrl + A,然后按 " |
列出所有窗口(双引号键) |
Ctrl + A,然后按 A |
重命名当前窗口(大写字母A) |
安装
1 | sudo apt install tmux |
| tmux new -s wordswar-fantasy | 创建 |
|---|---|
| tmux attach -t wordswar-fantasy | 进入 |
| tmux ls | 查看 |
| tmux kill-session wordswar-fantasy | 杀死 |
tmux attach可简写成tmux a,不指定-t时默认连接到最近使用的会话(如果有多个会话,会交互选择)
会话内快捷键
Ctrl+B,然后D |
分离 |
|---|---|
Ctrl+B,然后C |
新建窗口 |
Ctrl+B,然后N |
下一个窗口 |
Ctrl+B,然后P |
上一个窗口 |
Ctrl+B,然后W |
窗口列表 |
Ctrl+B,然后, |
重命名窗口 |
Pane(分屏)
Ctrl+B,然后% |
左右分屏 |
|---|---|
Ctrl+B,然后" |
上下分屏 |
Ctrl+B,然后方向键 |
在pane之间切换 |
| exit | 关闭pane |
配置文件~/.tmux.conf
1 | # 开启鼠标支持 |
1 | // 安装npm |
1 | pm2 start dotnet --name wordswardev --cwd /home/wordswardev/wordswar-fantasy-dev/app/server/linux-x64 -- Main.dll -m Develop |
1 | // 先保存一遍 |
| pm2 list | 列出所有项目 |
|---|---|
| pm2 show {name} | 查看详情 |
| pm2 delete {name} | 删除 |
| pm2 logs {name} | 查看log |
花时间手撸了个行为树,现在通过做笔记的方式巩固一下学习的内容
总体来说,脚本 可以分为俩大类
类图如下
