咖啡丶七

自律给我自由

基础结构

文件状态

  1. Changes to be committed

    文件已经被git add <file>存放在暂存区,现在有两条路可以走

    • 提交:使用git commit -m ""提交
    • 恢复:使用git restore --staged <file>git reset <file>将文件复原到工作区,文件修改的内容会保留
  2. Changes not staged for commit

    文件被修改,但是还为被git add <file>

    • 提交到暂存区:使用git add <file>
    • 恢复:使用git restore <file>git checkout <file>将文件的内容恢复成上一次提交的内容
  3. Untracked files

    新建的文件,还未被跟踪。这种状态的文件可以使用.gitignore文件取消跟踪

    • 使用git add <file>跟踪文件
    • 使用.gitignore屏蔽文件
    • 直接删除文件

Git远程命令

基础命令

1
2
3
4
5
6
7
8
// 查看绑定了哪些远程仓库
git remote -v
// 删除远程的绑定
git remote remove <name>
// 将本地仓库的<branch_name>分支推送到远程仓库的<branch_name>分支上
git push -u <origin_name> <branch_name>
// 将远程仓库的branch_name拉取到本地工作区
git pull <origin_name> <branch_name>

推送

1
2
3
4
# 将本地的<branch_name>分支推送到远程的<branch_name>分支上
git push <origin_name> <branch_name>
# 将本地的分支推送到远程分支上
git push <origin_name> <本地分支>:<远程分支>

还可以添加-u参数设置默认的推送和拉取映射关系
使用这个参数之后可以直接使用git pushgit pull而不用再添加参数

1
2
3
4
5
# 将本地的master分支推送到origin的master分支上,并设置这个为默认的推送和拉取映射
git push -u origin master
# 后面如果需要推送或者拉取可不用携带参数,默认为origin远程的master分支
git push # 将本地的master分支推送到远程的master分支上
git pull # 将远程的master分支拉取到本地的master分支上

注意:
经过测试发现携带-u参数时使用master:main的映射方法,后续使用git push无法成功的将本地的master分支推送到远程的main上
也就是说,如果使用了-u参数,那么需要推送到的远程分支名必须与本地分支名一致

拉取

将远程分支上的内容和到本地分支

有两种方法git pullgit fetch
两者的关系是:git pull是执行了git fetchgit merge两个操作

pull命令
1
2
3
4
# 将远程的<branch_name>分支拉取到本地的<branch_name>分支上
git pull <origin_name> <branch_name>
# 将远程的分支拉取到本地分支上
git pull <origin_name> <远程分支>:<本地分支>

可直接使用git pull <origin_name> <branch_name><origin_name>/<branch_name>的内容直接合并到本地

fetch命令
1
2
git fetch <origin_name> <branch_name>
git merge FETCH_HEAD
  1. 使用git fetch <origin_name> <branch_name><origin_name>/<branch_name>的内容保存到.git/FETCH_HEAD文件中
  2. 使用git merge FETCH_HEADFETCH_HEAD中的信息合并到当前分支
将远程分支拉取到本地作为一个新的分支

git checkout -b <new_local_branch> <origin/remote_banch_name>

在本地创建一个<new_local_branch>分支并将<origin/remote_banch_name>

一般要使用这个命令是因为遇到了冲突,详细处理方法可以查看另一篇文章【Git】合并分支及如何解决冲突


查看log

1
2
3
4
5
6
7
8
9
10
# 查看所有分支的提交记录之间的关系(以一行显示)
git log --oneline --graph --all
# 查看每个提交记录修改了哪些文件
git log --stat
# 查看每个提交记录具体的修改内容
git log -p
# 指定显示多少条日志
git log -2
# 指定跳过多少条日志
git log --skip=3

仓库文件

获取仓库大小

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$:正则匹配


记住账号密码

在linxu中每次都要输入账号密码,使用此命令后,push的时候再输入一次密码就不用再输入了

1
git config --global credential.helper store

中文为ASCII码

默认情况下git中文显示为ASCII码,使用该命令之后能正常显示中文

1
git config --global core.quotepath false

CRLF will be replaced by LF the next time Git touches it

这个警告提示的意思是,Git 在下次处理文件(例如提交或检出)时,会将文件中的换行符从 CRLF转换为 LF

背景

Git 对不同操作系统的换行符处理有不同的默认设置。通常,Git 会尝试在不同操作系统之间处理换行符的差异。例如:

  • Windows 使用 CRLF 作为换行符。
  • Unix/Linux/macOS 使用 LF 作为换行符。

解决方案

  1. 保持默认行为: 如果你不介意这种转换行为,可以忽略这个警告。Git 会在提交时自动处理换行符。

  2. 设置 Git 配置: 你可以通过全局 Git 配置或者设置 .gitattributes 文件来控制换行符的处理方式。

全局Git配置:在git shell上执行(使用了这个就不用配置.gitattributes文件了)

1
git config --global core.autocrlf true  # Windows 用户推荐

配置 .gitattributes 文件: 在仓库根目录下创建或编辑 .gitattributes 文件,添加以下规则来强制 Git 使用一致的换行符:(这个配置会覆盖全局设置)

1
2
# 强制所有文本文件使用 CRLF 作为换行符
* text=auto eol=crlf
  1. 手动转换换行符: 如果你需要手动处理换行符,可以使用文本编辑器或命令行工具来转换文件的换行符。
1
sed -i 's/\r$//' path/to/file.text

补救措施

如果之前已经提交过文件了,可以按照以下步骤重新设置文件的换行符

  1. 配置.gitattributes文件

    1
    2
    # 强制所有文本文件使用 LF 作为换行符
    * text=auto eol=crlf
  2. 删除Git缓存文件

    1
    git rm --cached -r .

    这个命令会将所有已跟踪的文件从 Git 的暂存区中移除,但不会删除工作目录中的文件。此时,Git 会认为这些文件已被删除。

  3. 重新添加所有文件到暂存区并提交更改

    1
    2
    git add .		# 此时可能依然还会报警告,但是不要紧。我们只要存储一次,修改文件在git中的规则之后,下次就不会有这个警告了
    git commit -m "Normalize line endings using CRLF"

    此时,Git 会根据 .gitattributes 文件中指定的换行符规则重新处理这些文件,并将它们添加回暂存区。

  4. 验证更改

    1
    git ls-files --eol

    如果输出的内容是,以下内容就没问题

    1
    2
    i/lf    w/crlf  attr/text=auto eol=crlf .gitattributes
    i/lf w/crlf attr/text=auto eol=crlf tset.text
    • i/lf:当前工作副本中的换行符类型因为使用的是git bash,是Unix,所以工作副本里是lf
    • w/crlf: Git 将在下次检出(checkout)或其他操作时将会使用的换行符类型
    • attr/text=auto eol=crlf:这是 .gitattributes 文件中的设置

    也可以将文件使用NotePad++rider等编辑器打开,查看右下角的编码格式

完全消除警告

经过测试发现,在创建Unity的C#脚本的时候.cs文件默认是crlf.meta文件使用的lf

所以只要你启用了Git全局配置的自动转换,或则在.gitattributes中指定了规则,你都无法完全避免这个警告。

唯一能完全取消这个警告的方法是:不再规范所有文件的换行规则

具体的做法就是:

  1. git config --global core.autocrlf false:取消自动转换
  2. .gitattributes注释掉换行规则

这样才能完全让这个警告消失在你的视野中

但正如“有得便有失”,你眼睛不会被脏了,但是代价是什么呢?

注意:

不再规范所有文件的换行规则后

  • 跨平台使用仓库,会影响文件的内容,从而影响版本控制和合并
  • 多人员开发时,没有规范换行规则也会影响文件内容,从而影响版本控制

所以只有在个人独自开发,且没有多平台需求的开发项目才能完全避免这个警告。

2024.10.9日更正:

可以更改unity的换行符格式

Project Setting -> Editor -> Asset Serialization -> Mode

  • Mixed:混合模式
  • Force Binary:.meta等文件将使用CRLF换行符格式
  • Force Text:meta等文件将使用LF换行符格式
持续更新

标题虽然是小技巧,但本篇文章里还是记录了一些常用的功能,本文章的主要目的是方便日后快速查找


Linq

Where

Sort

时间复杂度为O(nlogn)

默认排序

1
2
int[] numbers = { 5, 2, 8, 3, 1 };
Array.Sort(numbers); // 排序后:{1, 2, 3, 5, 8}

自定义比较器

1
2
string[] names = { "Alice", "Bob", "Charlie" };
Array.Sort(names, (x, y) => y.Length.CompareTo(x.Length)); // 按字符串长度降序

多条件排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

var people = new List<Person>
{
new Person { Name = "Alice", Age = 30 },
new Person { Name = "Bob", Age = 25 },
new Person { Name = "Charlie", Age = 25 }
};

// 按年龄升序,再按姓名升序
people.Sort((x, y) =>
{
int ageComparison = x.Age.CompareTo(y.Age);
return ageComparison == 0 ? x.Name.CompareTo(y.Name) : ageComparison;
});

部分排序

1
2
3
List<int> numbers = new List<int> { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
// 仅对索引 2 到 6 进行排序
numbers.Sort(2, 5, Comparer<int>.Default); // {10, 9, 3, 4, 5, 6, 8, 7, 2, 1}

拓展:

1
public delegate int Comparison<in T>(T x, T y);

输入参数

  • xy 是要比较的两个对象,类型为 T

返回值

  • 一个整数,用来表示排序顺序:
    • 负数:表示 x 小于 yx 排在 y 前面)。
    • :表示 x 等于 y(两者顺序不变)。
    • 正数:表示 x 大于 yx 排在 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():参数的名称(编译时确定好的常量),如果是泛型参数则输出T
  • typeof().Name:类名的名称(编译时确定好的常量),如果是泛型参数则会输出泛型类实例化时的泛型类型(泛型函数的泛型参数的泛型参数)如果参数不是泛型参数,则与nameof()的效果一样
  • obj.GetType().Name:实时获取obj的类型

测试代码:

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
public class Item{}
public class ItemA : Item{}
public class ItemB : Item{}

public class Test : MonoBehaviour
{
private Test<Item> test = new();
private void Awake()
{
Debug.Log($"{nameof(Item)} {nameof(ItemA)} {nameof(ItemB)}"); // Item ItemA ItemB
Debug.Log($"{typeof(Item).Name} {typeof(ItemA).Name} {typeof(ItemB).Name}"); // 同上
test.FuncTO<Item>(); // TO Item
test.FuncTO<ItemA>(); // TO ItemA
test.FuncTO<ItemB>(); // TO ItemB
test.FuncT(new Item()); // T Item Item
test.FuncT(new ItemA()); // T Item ItemA
test.FuncT(new ItemB()); // T Item ItemB
}
}

public class Test<T>
{
public Test()
{
Debug.Log($"构造函数: {nameof(T)} {typeof(T).Name}"); // T Item
}

public void FuncTO<TO>()
{
Debug.Log($"FuncTO: {nameof(TO)} {typeof(TO).Name}");
}

public void FuncT(T item)
{
Debug.Log($"FuncT: {nameof(T)} {typeof(T).Name} {item.GetType().Name}");
}
}


链式方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ChainedClass
{
public string name;
public int age;

public ChainedClass(string name) // 通过构造函数赋值
{
this.name = name;
}

public ChainedClass SetAge(int age) // 通过链式方法赋值
{
this.age = age;
return this;
}
}
1
2
3
4
5
ChainedClass cc = new ChainedClass("coffee")		// 构造函数
.SetAge(10); // 链式方法
Console.WriteLine(cc.Age); // 输出10
cc.SetAge(20); // 再次使用链式方法
Console.WriteLine(cc.Age); // 输出20

扩展方法

1
2
3
4
public static class DotsExtensions
{
public static float3 V2ToF3(this Vector2 v2) => new float3(v2.x, v2.y, 0);
}

Operator重载运算符

重写+-*/|$!===,完全由自己定义,返回的类型甚至不需要是自身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public struct CustomStruct		// 结构体或类都可以
{
public float X, Y, Z;

public static CustomStruct operator +(CustomStruct a, CustomStruct b)
{
return new CustomStruct { X = a.X + b.X, Y = a.Y + b.Y, Z = a.Z + b.Z };
}

public override string ToString()
{
return $"({X}, {Y}, {Z})";
}
}

可以像数字一样直接使用+法了

1
2
3
4
CustomStruct cs1 = new CustomStruct() { X = 1, Y = 2, Z = 3 };
CustomStruct cs2 = new CustomStruct() { X = 4, Y = 5, Z = 6 };

Console.WriteLine($"cs1 + cs2 = {cs1 + cs2}"); // 运算时自动调用`operator +`方法

implicit隐式类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public struct PlayerMoveInput
{
public float Value;

public static implicit operator PlayerMoveInput(float value)
{
return new PlayerMoveInput() { Value = value };
}

public static implicit operator float(PlayerMoveInput input)
{
return input.Value;
}
}
1
2
3
4
5
6
var input = new PlayerMoveInput();

input = 1; // 隐式转换 float -> PlayerMoveInput

// 需要注意我们这里并没有重写ToString,但是依然输出了1,一般是输出`PlayerMoveInput`
Console.WriteLine(input); // 隐式转换 PlayerMoveInput -> float

explicit显式类型转换

和隐式转换的语法一样,将implicit换成explicit就OK了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PlayerMoveInput
{
public float Value;

public static explicit operator PlayerMoveInput(float value)
{
return new PlayerMoveInput() { Value = value };
}

public static explicit operator float(PlayerMoveInput input)
{
return input.Value;
}
}
1
2
3
4
5
var input = new PlayerMoveInput();

input = (PlayerMoveInput)1; // 显示转换 float -> PlayerMoveInput

Console.WriteLine((float)input); // 输出1 显式转换 PlayerMoveInput -> float

访问权限

私有化构造函数

单例类,是不需要外界实例化的,可以将构造函数私有化,外界就无法实例化了。

1
2
3
4
5
class Manager
{
public static Manager Instance = new Manager(); // 单例类
private Manager() { } // 私有化构造函数
}

接口封装

不希望外界访问这个接口里所有的方法时,可以显示实现接口

1
2
3
4
5
6
public interface IInterface
{
void Add();
void Remove();
int Get();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ReadOnlyClass : IInterface						// 创建一个只能读取,不能更改内容的类
{
void IInterface.Add() // 显示接口实现,外界无法访问
{
throw new NotSupportedException("Readonly Not Supported Add"); // 就算访问了,也会抛异常
}

void IInterface.Remove() // 显示接口实现,外界无法访问
{
throw new NotSupportedException("Readonly Not Supported Remove"); // 就算访问了,也会抛异常
}

public int Get() // 正常访问Get()方法
{
return 1;
}
}

ReadOnlyDictionary<TKey, TValue>()ReadOnlyList<>ReadOnlyCollection<>ReadOnlySet<>都是使用显示实现接口的方法隐藏了修改的方法,使外界只能读取,不能修改

完整使用方法:

1
2
3
4
5
6
7
8
9
class Manager
{
private Dictionary<string, string> _uiStack;
public ReadOnlyDictionary<string, string> UIStack { get; } // 注意这里没有使用set,完全杜绝了修改的可能
public Manager()
{
UIStack = new ReadOnlyDictionary<string, string>(_uiStack);
}
}

拓展:

{ get; private set; }:在类内部还能重新setter

{ get; }:只能在构造函数中setter


列表

自动排序列表

创建一个按照特定规则排列的列表:

例如,实例化一些Human,并且将他们按照age从大到小的方式放置一个列表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface IAnimal
{
public int age { get; set; }
public string name { get; set; }
}
public class Human : IAnimal
{
public int age { get; set; }
public string name { get; set; }

public Human(int age, string name)
{
this.age = age;
this.name = name;
}
}

定义一个单例泛型类,用来做比较

1
2
3
4
5
6
public class ReverseComparer<T> : IComparer<T>
{
public static readonly ReverseComparer<T> Instance = new ReverseComparer<T>(); // 饿汉单例
private ReverseComparer() { } // 私有构造函数,不需要用户创建实例
public int Compare(T x, T y) => Comparer<T>.Default.Compare(y, x); // 实现接口,定义比较方法,注意参数换位置了
} // 如果想要从小到大排列,再换回来就好了
1
2
3
4
5
6
7
public class HumanSelector : SortedList<int, IAnimal>		// 继承`SortedList<int, IAnimal>`基类
{
public HumanSelector() : base(ReverseComparer<int>.Instance) {} // 构造函数,并将参数传递给基类的构造函数
public void Add<T>(T man) // 定义了一个泛型方法
where T : IAnimal // 这个泛型类必须是来自IAnimal接口的
=> Add(man.age, man); // 调用了基类的Add方法
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
Human A = new Human(20, "A");
Human B = new Human(25, "B");
Human C = new Human(30, "C");
HumanSelector humanSelector = new HumanSelector(); // 实例化排序列表
humanSelector.Add(C);
humanSelector.Add(A);
humanSelector.Add(B);
humanSelector.ToList().ForEach(man => { Console.WriteLine($"{man.Key}, {man.Value.name}"); });

// 30, C
// 25, B
// 20, A

这样,我们就将排序封装起来了,使用者只需要使用Add将元素添加到排序列表中就OK了

遍历列表删除符合要求的元素

  • 使用常规方法遍历列表直接删除元素会导致遍历循环顺序错误
  • 复制列表会占用内存

最快速也是最简单的方法:反向遍历,从大索引号开始遍历

1
2
3
4
5
6
7
8
List<int> list = new List<int>(){ 1, 2, 3, 4, 5 };
for (int i = list.Count - 1; i >= 0; i--)
{
if (list[i] >= 4)
{
list.RemoveAt(i);
}
}

字段与属性

索引器

总得来说大致有三种用法

  1. 最普通的,针对字段复制了一个属性供外界使用,每次访问都会创建一个值类型的副本。
1
2
3
4
5
6
private int _count;							// 字段:内部使用
public int Count // 属性:外部可读可写
{
get => _count;
set => _count = value / 2; // 可自定义读写需求
}
1
2
private int _length;
public int Length => _length; // 属性:公共只读
1
public static int TotalCount { get; set; }		// 属性:可读可写,这种方式实际上也隐式的创建了一个字段,如下图

  1. 使用了ref修饰,使Run实际上是直接访问的_run这个字段,减少了一个复制的过程,提高性能
1
2
3
[SerializeField]
private bool _run; // 内部使用
public ref bool Run => ref _run; // 外部使用,这里实际上是直接访问的_run这个字段
  1. 可以让类像列表一样访问
1
2
3
4
5
public string this[int index]
{
get => _items[index];
set => _items[index] = value;
}

甚至还可以将字典改成与python一样的用法:不过不推荐这种用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CustomizeDict
{
private Dictionary<string, string> _data = new Dictionary<string, string>();
public object this[string key]
{
get => _data[key];
set
{
if (_data.ContainsKey(key))
_data[key] = value.ToString();
else
_data.Add(key, value.ToString());
UpdateContent();
}
}
}

CustomizeDict["speed"] = 500;

default

常用default能使代码排版更好看,可以省去写判断表达式

值类型

1
2
3
int number = defaule;   // 默认值为0
double amount = default// 默认值为0
bool flag = default; // 默认值为false

引用类型

1
2
string text = default;		// 默认值为null
List<int> numbers = defalut; // 默认值为null

字符串、数组切片

C#在8.0后字符串和数组可以像python一样使用切片,时代码变的即简单又美观

值得注意的是列表不能使用这个小技巧

示例:

1
2
3
4
5
6
7
8
9
10
var str = @"HelloWorld";

Console.WriteLine(str[1..5]); // "ello"

Console.WriteLine(str[^1]); // "d"
Console.WriteLine(str[..^0]); // "HelloWorld"

Console.WriteLine(str[6..^2]); // "or"
Console.WriteLine(str[^5..^1]); // "Worl"
Console.WriteLine(str[^7..8]); // "loWor"
  • 这里的^python里的-一样:反着数,^1表示最后一个字符d^5表示倒数第五个字符W
  • 左闭右开,包含第一个参数,不包含第二个参数

简写判断语句

这里面的所有简写rider都会提示,部分简写不建议使用,大大提高的代码的阅读效率

一个数介于两个数之间

1
2
3
4
5
var ran = new Random();
int i = ran.Next(20); // 在0-20之间随机选一个数

if (i is < 10 and > 0)
Console.WriteLine("i介于0和10之间");

值得注意一点的是,这个判断的两个数必须是常量值

如果是list.Count,这里就用不了了

循环判断

为方便理解我先写成这样,解释一下每个方法的含义

1
2
3
4
var list = new List<int>() { 5, 2, 7, 1, 9, 4 };		// 定义一个int列表
IEnumerable<int> temp = list.Where(i => i > 3); // 获取这个列表中大于3的元素的IEnumerable<int>枚举
list = temp.ToList(); // 将IEnumerable<int>枚举转换成列表
list.ForEach(Console.WriteLine); // 使用方法组的方式逐个打印结果

可简写成如下

1
2
3
4
5
var list = new List<int>() { 5, 2, 7, 1, 9, 4 };
foreach (var j in list.Where(i => i > 3)) // 提一嘴,里的i和j其实是分隔开的,并不会相互影响,j可以写成i
{
Console.WriteLine(j);
}

老实做法:

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
2
3
int count = 100;
Console.WriteLine("我今天吃了{0}顿", count)
Console.WriteLine($"我今天吃了{count}顿")

第二种方法的性能要比第一种好

但是第一种情况在一些特殊的情况下使用有奇效,比如多语言设置、字符串替换等功能

具体可以看C#中字符串类的一些实用技巧及新手常犯错误这个大佬的

以上用的算的比较多的,所以不是今天的重点,今天的重点在格式化插值和格式化输出

1
2
3
double i = 32.53728012341241562;

Console.WriteLine($"我的资产为{i, 10:F4}为所欲为"); // "我的资产为 32.5373为所欲为"
  • 10表示占位10个位置,当位置不够的时候在前面用空格补充
  • F4表示保留4位小数,F不区分大小写

除了F外还有其他很多格式:

数字格式

数字格式都不区分大小写

  • F:Fiexd-point格式。显示小数后几位。$"{1234567.89:F2}" => 1234567.89
  • C:货币格式。显示货币符号和千位分隔符。$"{1234.56:c}" => ¥1,234.56
  • E:科学计数法格式。显示非常大或非常小的数字。$"{1231421432:e3}" => 1.231e+009
  • N:与F类似,但是N能显示整数上的千位分隔符。$"{21432.537280:n3}" => 21,432.537
  • G:自动选择合适的格式。
  • P:百分比格式。将数值乘以100,并在结果后面加上百分号。$"{0.12345:p2}" => 12.35%
  • R:常规格式。不带任何格式的方式显示数字,但保留足够的精度(怎么保留没有仔细研究,简单测试了几下,没有找出规律)

实用方法:

1
2
float value = 546.5198;
string str = $"{value:0.##}"; // 保留两位小数,当小数不足时不显示(如,1.2 => 1.2 而不是1.20)

日期时间格式

与数字格式不同,日期时间格式需要区分大小写

注意:码代码的时候一定要拼写正确,是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

  • Mm:月日格式。$"{date:m}" => 8月11日

  • Yy:年月格式。$"{date:m}" => 2024年8月

另外还有十分强大的自定义格式化

DateTime的格式化:

1
2
DateTime date = DateTime.Now;
Console.WriteLine($"{date:yyyy年mm月dd日 hh:mm:ss tt zzz}");

输出:

1
2024年05月11日 05:05:09 上午 +08:00

TimeSpan的格式化:

1
2
3
TimeSpan timeSpan = endTime - DateTime.Now;
timeSpan.ToString(@"d\天hh\时mm\分"); // '\'必不可少,表示后面一位是普通字符
timeSpan.ToString(@"d\:h\:mm);

输出

1
2
// 1天03时15分
// 1:3:15分

Unity

性能测试

使用Profiler.BeginSample(string)Profiler.EndSample()方法捕获代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void Update()
{
Profiler.BeginSample("Invoke Action");
for (int i = 0; i < 1000000; i++)
{
actionA?.Invoke();
}
Profiler.EndSample();

Profiler.BeginSample("Invoke default Action");
for (int i = 0; i < 1000000; i++)
{
actionB.Invoke();
}
Profiler.EndSample();
}

介绍

DOTween官网

  • TweenerSequenceTween的子类
  • Tweener是播放的单个动画
  • Sequence控制Tweener的播放

容易踩坑点

  • 一个Tweener的循环为3时,那么他们三个算一个完整的动画

  • Tweener添加到Sequence之后Tweener的大部分属性设置会被覆盖,比如循环,Pause()

  • 最容易照成的误区,动画为从原点向前移动5,Restart()之后将重复刚才的动画,也就是说他会先回到原点再向前移动5

  • 如果为设置SetAutoKill(false)会在动画播放完一次之后就销毁掉,并且使用_sequence.Restart()并不会有反应,如果动画不是第一次播放,并且没有效果的话,可以检查一下定义这个动画的时候有没有使用SetAutoKill(false)。但是在使用SetAutoKill(false)之后,记得在合适的时候使用_sequence = null销毁掉对象,避免内存泄漏


Tweener

控制播放周期

  • 可以将其想象成视频的播放按钮,打开一个视频的时候他会开始自动播放,除非暂停。

  • 暂停之后,需要点击Play继续播放

  • 视频放完之后再次点击Play没用,只能Restart。

Tweener方法如下

  • Play():继续播放动画,在实例化Tweener的时候就会
  • Pause():暂停动画,使用Play()继续播放
  • PlayForward():动画向前播放
  • PlayBackwards():动画向后播放
  • Restart(bool, float):重新开始播放动画;是否忽略Sequence
  • Kill(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private DOGetter<float> _radiusGetter;	// 其实就是一个获取数值的委托
private DOSetter<float> _radiusSetter; // 其实就是一个设置数值的委托

private void Awake()
{
_radiusGetter = () => _light.pointLightOuterRadius; // 定义委托
_radiusSetter = newValue => _light.pointLightOuterRadius = newValue; // 定义委托

InputReader.Instance.AmplifyPressedEvent += AmplifyLight; // 设置触发事件
}

private void AmplifyLight()
{
DOTween.To(_radiusGetter, _radiusSetter, newValue, duration);// 执行DOTween动画——改变_radiusGetter获取的值
}

Sequence使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Sequence bounceSequence;

bounceSequence = DOTween.Sequence()
// 添加一个向下移动的Tweener
.Append(rect.DOAnchorPosY(rect.anchoredPosition.y - 150f, .1f).SetEase(Ease.OutQuad))
// 添加一个还原的Tweener
.Append(rect.DOAnchorPosY(rect.anchoredPosition.y, .2f).SetEase(Ease.OutQuad))
.Pause()
.SetAutoKill(false);

public void OnPointerClick(PointerEventData eventData)
{
bounceSequence.Restart(); // 使用Restart(),表示这是一个可重复触发的动画
}

倒放动画

1
2
3
4
5
6
7
8
9
10
11
12
13
private Tweener amplifyTween;

amplifyTween = rect.DOScale(1.2f, 0.2f).SetAutoKill(false).Pause();

public void OnPointerEnter(PointerEventData eventData)
{
amplifyTween.PlayForward(); // 鼠标进入时,放大图片
}

public void OnPointerExit(PointerEventData eventData)
{
amplifyTween.PlayBackwards(); // 鼠标离开时,倒放动画,即还原
}

终止动画

当前dotween动画没播放完,便再次播放有冲突的操作,如连续多次播放、正播、倒播,导致显示不正常或报错。

解决方法:在每次开始执行播放动画时,先加上下面对应类似的杀死进程代码,就OK了

1
2
transform.DOKill();
transform.RectTransform().DOKill();

忽略timeScale影响

让DOTweenAnimation忽略Time.timeScale = 0的影响

1
tween.SetUpdate(true);

360度旋转

设置Rotate旋转模式

1
2
transform.DOLocalRotate(new Vector3(0, 0, -360), 2, RotateMode.FastBeyond360)
.SetEase(Ease.Linear).SetLoops(-1, LoopType.Restart).Play();

Form()

DOTween的参数默认都是目标值,使用From后参数代表起点

1
2
3
4
5
6
// 绝对位置,若当前坐标(1,0,0),即从5运动到1
transform.DOMoveX(5, 1).From();
transform.DOMoveX(5, 1).From(false);

// 相对位置,若当前坐标(1,0,0),即从6运动到1(6-1=5,相对位移5)
transform.DOMoveX(5, 1).From(true);

SetLoops(int loops, LoopType loopType)

  • loops:循环次数,-1为无数次循环
  • loopType:循环模式
    • Restart:从头开始循环(默认)
    • Yoyo:交替来回移动
    • Incremental:设置为相对运动后才生效,连续“向前”移动(A到B, B到B+(A-B), …)

SetRelative()

设置为相对运动

SetEase

这里可以参考 👉 | https://easings.net | 👈 上的曲线效果

Flash

示例


UniTask配合

想要将DoTween转换成UniTask,需要在ProjectSetting - Player - OtherSetting - ScriptingDefineSymbols中添加UNITASK_DOTWEEN_SUPPORT

优点:

  • 可以使用await,而不是OnComplete回调函数,使代码更直观

    1
    2
    3
    4
    5
    6
    private async void Start()
    {
    await transform.DOMove(new Vector3(5f, 0, 5f), 2); // 先移动
    await transform.DORotate(new Vector3(90f, 90, 90f), 2); // 再旋转
    Debug.Log("Complete"); // 最后再输出
    }
  • 可以使用WithCancellation方法取消DOTween

    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 CancellationTokenSource _cts = new CancellationTokenSource();

    private async void Start()
    {
    await transform.DOMove(new Vector3(5f, 0, 5f), 2).WithCancellation(_cts.Token);
    Debug.Log("Complete");
    }

    private void Update()
    {
    if (Mouse.current.leftButton.wasPressedThisFrame)
    {
    _cts.Cancel();
    }
    }

    private void OnDestroy()
    {
    _cts?.Dispose();
    }
    }

注意:

  • 如果想要重复使用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();
    }
    }
    }

参考

Unity Dotween插件的运动曲线(Ease)介绍Ease选项Ease效果示例以及C#修改动画曲线功能

介绍

DOTween官网教程


Inspector

比较简单,看示例就能懂

自定义Editor还挺好用的,后面有机会补


Validator

总的来说大概有两种使用方法

  1. 不使用语法糖,检测全局物体
  2. 使用语法糖,检测指定的属性

有两种创建方式RegisterValidatorRegisterValidatorRule。这两个唯一不同的是,后者可以在Validator窗口上编辑是否禁用和配置其属性

Value Validator

检测全局的

比如说,相机必须要绑定角色,就可以通过这个方式来检测,具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#if UNITY_EDITOR
using Sirenix.OdinInspector.Editor.Validation;

[assembly: RegisterValidator(typeof(CameraValidator))]

public class CameraValidator : ValueValidator<PlayerCamera> // 我们需要检验所有的PlayerCamera脚本
{
protected override void Validate(ValidationResult result)
{
if (Value.player == null) // 判断PlayerCamera.player是否为空
result.AddError("Camera need player");
}
}
#endif

public class PlayerCamera : MonoBeahaviour
{
public Transform player;
}

将这个脚本放在项目中就可以了,不用做其他操作。然后Validator就会找寻场景中所有使用了PlayerCamera脚本的实例,检查player是否为空

如果没有绑定角色,就会出现下图Error

Root Object Validator

Root Object Validator非常适合验证Unity对象,例如ScriptableObject、Materials和Components。因为您只会收到对象本身的告警/错误消息,所以称为Root

Value Validator不同,在另一个对象中引用跟对象,不会引发额外的警告/错误

Attribute Validator

属于第一类,检测指定属性的


Serializer

序列化用的,如果要实现存档,建议使用ES3

之前写了一篇介绍事件和委托的帖子,但是没怎么运用过,只是使用一些简单的事件

这次看了一个项目的源码,趁着这个机会好好研究一下事件和委托的作用

event的作用

1
2
public Action Action_A;
public event Action Action_B;
  • Action:安全性较差,外部代码可以触发这个委托(使用.Invoke()
  • event Action:安全性强,外部只能订阅或取消订阅(使用-=+=),不能直接触发事件

使用情景

假设现在有两个类ClassAClassB,在以下情况推荐使用事件委托来实现

  1. 需要在特定的时机触发某个方法
  2. 需要使用其他实例的私有属性
  3. 某个方法的实现有两种限制,比如UI中需要该物体处于激活状态,且按下手柄的A键

前面两个都还比较好理解,第三种情况除了事件本身Invoke()调用的时机外,还可以通过注册(+=)注销(-=)来控制这个事件中是否有任务来控制释放的时机

需要在特定时机时触发某个方法

场景:

  • ClassA需要特定的时候(比如参数intA等于0、或某个按钮被按下)执行方法FuncA

具体操作:

  1. 创建一个事件ActionA
  2. 初始化的时候注册这个事件ActionA += FuncA
  3. intA等于0时触发这个事件ActionA.Invoke()

示例代码:

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
public class ClassA : MonoBehaviour		// 这里需要引用UniTask插件,为了简洁我就不写了引用了
{
public int intA;
private Action ActionA;

private void Start()
{
ActionA += FuncA; // 注册事件,并不会执行FuncA方法,只有在A.ActionA被Invoke的时候才会执行FuncA
UpdateNumber().Forget();
}

private async UniTask UpdateNumber()
{
while (true)
{
await UniTask.Delay(TimeSpan.FromSeconds(.5f), ignoreTimeScale: false);
// 当第一个实参大于等于11时,返回0;当第一个实参小于0时返回11;其他则返回第一个实参。
// 这里--在前面,所以是先自减,再将结果当做Repeat的第一个实参
intA = (int)Mathf.Repeat(--intA, 11);

if (intA == 0) //第一个实参为0时也返回0,所以intA是能等于0的
ActionA?.Invoke();
}
}

private void FuncA()
{
Debug.Log($"当前值为: {intA}");
}
}

需要使用其他实例的私有属性

场景:

  • ClassA需要特定的时候(比如参数intA等于0、或某个按钮被按下)获取其他类中的私有方法FuncB或参数strB

具体操作:

  1. ClassA创建一个事件ActionA

  2. ClassB中注册这个事件ClassA.ActionA += FuncB

  3. intA等于0时ActionA.Invoke()

示例代码:

ClassA中的intA等于0时,就会执行ClassB中的FuncB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ClassA : MonoBehaviour
{
public int intA;
public Action ActionA;

private void Start()
{
UpdateNumber().Forget();
}

private async UniTask UpdateNumber()
{
while (true)
{
await UniTask.Delay(TimeSpan.FromSeconds(.5f), ignoreTimeScale: false);
intA = (int)Mathf.Repeat(--intA, 11);

if (intA == 0)
ActionA?.Invoke();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ClassB : MonoBehaviour
{
private string strB = "I am private"; // B内部使用的参数,“我不能暴露出去哦”
public ClassA A; // 在Inspector窗口中引用实例

private void Start()
{
A.ActionA += FuncB; // 注册事件,并不会执行FuncA方法,只有在A.ActionA被Invoke的时候才会执行FuncA
}

private void FuncB()
{
Debug.Log(strB);
}
}

某个方法的实现有两种限制

场景:

  • ClassA需要特定的时候(比如参数intA等于0、某个按钮被按下),且B处于激活状态时执行方法FuncA

具体操作:

  1. ClassA创建事件ActionA
  2. 在激活B时将FuncA注册给ActionA,停用B时将FuncA注销掉

示例代码:

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
public class ClassA : MonoBehaviour
{
public int intA;
public Action ActionA;

private void Start()
{
UpdateNumber().Forget();
}

private async UniTask UpdateNumber()
{
while (true)
{
await UniTask.Delay(TimeSpan.FromSeconds(.5f), ignoreTimeScale: false);
intA = (int)Mathf.Repeat(--intA, 11);

if (intA == 0)
ActionA?.Invoke();
}
}

public void FuncA()
{
Debug.Log("我只有在intA等于0且B处于激活状态的时候执行");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ClassB : MonoBehaviour
{
public ClassA A;

private void OnEnable()
{
A.ActionA += A.FuncA;
}

private void OnDisable()
{
A.ActionA -= A.FuncA;
}
}

试玩网站

为了方便观看将3DRPG游戏部署到了网站上https://www.coffeeofnosugar.top/unitygame/3drpgv3/

由于头发的物理模拟并不支持WebGL,所以头发看起来像塑料一样

介绍视频

行为树 & 第三人称控制器

行为树编辑器

使用UI Builder从零制作怪物行为树可视化编辑器,通过视图连接各个节点,实现定制化怪物AI。

  • 使用面向对象的方式规范节点代码,使代码清晰易维护
  • 使用UI Builder实现自定义节点外观,可更改各个节点显示文本内容、颜色和形状等
  • 使用UI Builder实现引擎窗口视图自定义化,展示自定义Inspector与Blackboard窗口,并实时更新展示的数据
  • 使用C#代码定制逻辑控制器节点
    • Repeat节点:重复执行子节点
    • Selector节点:依次执行子节点,直到所有子节点执行完毕或有一个子节点返回Success,Selector节点停止运行并且也返回Success
    • Sequencer节点:依次执行子节点,直到所有节点执行完毕或有一个子节点返回Failure,Sequencer节点停止运行并且也返回Failure
    • Weight节点:给每个子节点设置一个权重,按照权重选择一个子节点执行,并返回与该子节点一样的结构
  • 使用C#代码定制行为树动作节点
    • Wait节点:设置延迟时间,执行到该节点时在此停留设置的时间
    • TargetDistance节点:设置距离,判断与Target的距离,小于设置的距离返回Failure,大于设置的距离返回Success,通常与Selector节点一起使用
    • MoveToTarget节点:开启Run动画,设置移动速度,朝向Target移动,并根据agent的路径状态停止移动,以防卡住
    • RandomPosition节点:随机获取起始点附近的一个点位,通常与MoveToPosition节点一起使用
    • MoveToPosition节点:移动到RandomPosition节点随机获取到的点位上
    • Log节点:打印设置的message信息
    • Skill节点:通过在怪物节点中设置的技能cd和技能释放距离判断是否达到释放技能条件,达到释放条件后将释放此技能
  • 通过行为树窗口视图连接逻辑控制器节点和动作节点,实现定制化怪物AI
  • 在PlayerMode下可以通过行为树视图实时观察怪物当前状态

第三人称控制

  • CharacterController控制玩家移动
  • 有限状态机模式控制玩家状态
  • Input System监听玩家输入
  • Animator控制玩家动画,下一步计划将舍弃Animator转而使用Playable播放动画

创建文件/etc/systemd/system/qsign.service,注意Service的参数需要使用绝对路径

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=unidbg-fetch-qsign
After=network.target

[Service]
Environment="JAVA_HOME=/usr/local/btjdk/jdk-11.0.19"WorkingDirectory=/www/wwwroot/unidbg-fetch-qsign
ExecStart=bash bin/unidbg-fetch-qsign --basePath=txlib/8.9.70
Restart=always
RestartSec=60s

[Install]
WantedBy=multi-user.target

命令

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 持续输出所有后台服务器日志


命令

1
2
nginx -t  # 检测配置文件
nginx -s reload # 重载配置文件

配置文件

配置文件为:./nginx/conf/nginx.conf

静态文件为:./nginx/html/

location

用法

location [ = | ~ | ~* | ^~ ] uri { ... },其中|表示你可能会用到的语法

  • =:精确匹配
  • ~:区分大小写的正则匹配
  • ~*:不区分大小写的正则匹配
  • ^~:uri以某个字符串开头
1
2
3
4
5
local ^~ /unitygame/ {
root /usr/local/nginx/html;
}

# /unitygame/3drpg return /usr/local/nginx/html/unitygame/3drpg/index.html
  • /unitygame/3drpg:通用匹配
  • /:默认匹配
顺序

优先级:= > ^~ > ~ > ~* > 最长的通用匹配 > 默认匹配

  • 经测试“默认匹配”最好放在“通用匹配后面”

root 与 alias 的区别

root会与URI的剩余部分(例子中为”/i/“)拼接,而alias不会

1
2
3
4
5
6
7
8
9
10
11
localtion /i/ {
root /usr/local/nginx/html/blog;
}

# /i/top.gif renturn /usr/local/nginx/html/blog/i/top.gif;

localtion /i/ {
alias /usr/local/nginx/html/blog;
}

# /i/top.gif return /usr/local/nginx/html/blog/top.gif;

这里贴一个我自己的完整配置

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

#user nobody;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
listen 80;
server_name www.coffeeofnosugar.top coffeeofnosugar.top;
return 301 https://www.coffeeofnosugar.top;
}



# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}


# HTTPS server

server {
listen 443 ssl;
server_name www.coffeeofnosugar.top coffeeofnosugar.top;

ssl_certificate ../coffeeofnosugar.top.pem;
ssl_certificate_key ../coffeeofnosugar.top.key;

#ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;

#ssl_ciphers HIGH:!aNULL:!MD5;
#ssl_prefer_server_ciphers on;

location /blog/ {
root /usr/local/nginx/html;
}

location / {
root /usr/local/nginx/html/blog;
index index.html;
}

location ~ ^/(unitygame|godot|addressable)/ {
root /usr/local/nginx/html/;
}
}
}


参考连接

Nginx Location 配置讲解

花时间手撸了个行为树,现在通过做笔记的方式巩固一下学习的内容

总体来说,脚本 可以分为俩大类

  • 为ScriptableObject(行为树)服务的脚本
  • 为视图服务的Editor脚本

类图如下


2024-07-01 混合有氧 腹部


2024-07-02 塑形瘦身训练 腹部


2024-07-04 塑形瘦身训练 腹部


2024-07-05 混合有氧 腹部


2024-07-07 有氧运动 腹部


2024-07-08 塑性瘦身训练 腹部


2024-07-09 腹部 混合有氧


2024-07-10 腹部


2024-07-11 混合有氧 腹部


2024-07-12 腹部 混合有氧


2024-07-13 腹部 混合有氧


2024-07-14 腹部 塑性瘦身训练