初始化obs/Unity仓库
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"promptDelete": false,
|
||||
"alwaysUpdateLinks": true
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"cssTheme": "",
|
||||
"interfaceFontFamily": "",
|
||||
"baseFontSize": 19
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"file-explorer": true,
|
||||
"global-search": true,
|
||||
"switcher": true,
|
||||
"graph": true,
|
||||
"backlink": true,
|
||||
"canvas": true,
|
||||
"outgoing-link": true,
|
||||
"tag-pane": true,
|
||||
"properties": false,
|
||||
"page-preview": true,
|
||||
"daily-notes": true,
|
||||
"templates": true,
|
||||
"note-composer": true,
|
||||
"command-palette": true,
|
||||
"slash-command": false,
|
||||
"editor-status": true,
|
||||
"bookmarks": true,
|
||||
"markdown-importer": false,
|
||||
"zk-prefixer": false,
|
||||
"random-note": false,
|
||||
"outline": true,
|
||||
"word-count": true,
|
||||
"slides": false,
|
||||
"audio-recorder": false,
|
||||
"workspaces": true,
|
||||
"file-recovery": true,
|
||||
"publish": false,
|
||||
"sync": false,
|
||||
"webviewer": false,
|
||||
"footnotes": false,
|
||||
"bases": true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"collapse-filter": true,
|
||||
"search": "",
|
||||
"showTags": false,
|
||||
"showAttachments": false,
|
||||
"hideUnresolved": false,
|
||||
"showOrphans": true,
|
||||
"collapse-color-groups": false,
|
||||
"colorGroups": [],
|
||||
"collapse-display": true,
|
||||
"showArrow": false,
|
||||
"textFadeMultiplier": 0,
|
||||
"nodeSizeMultiplier": 2.55,
|
||||
"lineSizeMultiplier": 2.46522491349481,
|
||||
"collapse-forces": true,
|
||||
"centerStrength": 0.518713248970312,
|
||||
"repelStrength": 10,
|
||||
"linkStrength": 1,
|
||||
"linkDistance": 250,
|
||||
"scale": 0.36288736930121296,
|
||||
"close": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "AnuPpuccin",
|
||||
"version": "1.5.0",
|
||||
"minAppVersion": "1.6.0",
|
||||
"author": "Anubis",
|
||||
"authorUrl": "https://github.com/AnubisNekhet"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Minimal",
|
||||
"version": "7.6.0",
|
||||
"minAppVersion": "1.6.0",
|
||||
"author": "@kepano",
|
||||
"authorUrl": "https://twitter.com/kepano",
|
||||
"fundingUrl": "https://www.buymeacoffee.com/kepano"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"types": {
|
||||
"aliases": "aliases",
|
||||
"cssclasses": "multitext",
|
||||
"tags": "tags",
|
||||
"date": "datetime"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"main": {
|
||||
"id": "bd142ba1273e2db5",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "c2c64ff1a44cc113",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "46319f9ef341d8cf",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "VPS搭建节点.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "VPS搭建节点"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "vertical"
|
||||
},
|
||||
"left": {
|
||||
"id": "f2a85a2ac61b7a8f",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "7cde0997ce5c033e",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "693185ed16953a0f",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "file-explorer",
|
||||
"state": {
|
||||
"sortOrder": "byModifiedTime",
|
||||
"autoReveal": false
|
||||
},
|
||||
"icon": "lucide-folder-closed",
|
||||
"title": "文件列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7a82dde29f38ff76",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "search",
|
||||
"state": {
|
||||
"query": "tag:#BUG记录",
|
||||
"matchingCase": false,
|
||||
"explainSearch": false,
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical"
|
||||
},
|
||||
"icon": "lucide-search",
|
||||
"title": "搜索"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cfc16be7dcd2c261",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "bookmarks",
|
||||
"state": {},
|
||||
"icon": "lucide-bookmark",
|
||||
"title": "书签"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 277.5
|
||||
},
|
||||
"right": {
|
||||
"id": "3b0c248d3f706cf5",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "8ab3af319950e735",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "40d484ef9ec140ad",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"file": "VPS搭建节点.md",
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
"showSearch": false,
|
||||
"searchQuery": "",
|
||||
"backlinkCollapsed": false,
|
||||
"unlinkedCollapsed": false
|
||||
},
|
||||
"icon": "links-coming-in",
|
||||
"title": "VPS搭建节点 的反向链接列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fd67818c0f269d3c",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"file": "VPS搭建节点.md",
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-going-out",
|
||||
"title": "VPS搭建节点 的出链列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "3a8ef6dec519f74c",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "tag",
|
||||
"state": {
|
||||
"sortOrder": "frequency",
|
||||
"useHierarchy": true,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-tags",
|
||||
"title": "标签"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "39aaa41f1df3054f",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"file": "VPS搭建节点.md",
|
||||
"followCursor": true,
|
||||
"showSearch": true,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "VPS搭建节点 的大纲"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 317.5
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
"bases:新建数据库": false,
|
||||
"switcher:打开快速切换": false,
|
||||
"graph:查看关系图谱": false,
|
||||
"canvas:新建白板": false,
|
||||
"daily-notes:打开/创建今天的日记": false,
|
||||
"templates:插入模板": false,
|
||||
"command-palette:打开命令面板": false,
|
||||
"workspaces:管理工作区布局": false
|
||||
}
|
||||
},
|
||||
"active": "46319f9ef341d8cf",
|
||||
"lastOpenFiles": [
|
||||
"WezTerm.md",
|
||||
"VPS搭建节点.md",
|
||||
"零碎知识小计.md",
|
||||
"图片/deepseek_mermaid_20260204_d63c65.png",
|
||||
"CSharp学习/dot-net.md",
|
||||
"未命名.base",
|
||||
"多益训练营/从零开始做回合制游戏.md",
|
||||
"图片/Pasted image 20250525181358.png",
|
||||
"2025-07-13.md",
|
||||
"Protobuf使用.md",
|
||||
"开发过程BUG记录.md",
|
||||
"龙虾注册.md",
|
||||
"储蓄.md",
|
||||
"待办表.md",
|
||||
"12.域名解析.md",
|
||||
"多益训练营/Python学习.md",
|
||||
"多益训练营",
|
||||
"补强学习.md",
|
||||
"对话系统.md",
|
||||
"ReadeMe.md",
|
||||
"ARPG项目后续发展方案.md",
|
||||
"plan.md",
|
||||
"论文结构框架.md",
|
||||
"算法学习/面试算法题网搜.md",
|
||||
"项目学习/王国之梦制作/19.卡牌拖拽.md",
|
||||
"光照.md",
|
||||
"CSharp学习/CSharp入门查漏补缺.md",
|
||||
"CSharp学习/C-Sharp数据类型.md",
|
||||
"算法学习/灵山题单.md",
|
||||
"未命名 1.base",
|
||||
"计算机图形学",
|
||||
"图片/Pasted image 20260320020450.png",
|
||||
"算法学习",
|
||||
"日记",
|
||||
"未命名.canvas",
|
||||
"Unity学习/Unity组件/RigidBody/Pasted image 20241218120120.png",
|
||||
"未命名 1.canvas",
|
||||
"Unity学习/数据持久化",
|
||||
"CSharp学习",
|
||||
"就业",
|
||||
"项目学习",
|
||||
"Pasted image 20250621235440.png",
|
||||
"Pasted image 20250622000814.png",
|
||||
"Pasted image 20250621235840.png",
|
||||
"图片/LineRenderer.png",
|
||||
"图片/Pasted image 20250514200735.png",
|
||||
"图片/Pasted image 20250525175247.png",
|
||||
"Unity组件/未命名.canvas"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"workspaces": {},
|
||||
"active": ""
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
tags:
|
||||
- 网络开发基础
|
||||
date: 2025-12-29T09:32:00
|
||||
---
|
||||
## 什么是域名解析?
|
||||
|
||||
我们上网的本质就是在和不同的IP地址以及端口号进行通讯,根据需求不同有时候进行长连接通讯,有时候进行短链接通讯。在我们访问某个网页的时候,本质就是向他的IP地址以及80端口进行一次通讯,从而得到页面数据渲染下来。
|
||||
|
||||
可是IP地址作为纯数字,并不好记忆,于是诞生了域名技术,也就是将一个方便记忆的地址,他内部指向一个IP地址,通过DNS技术,将域名解析为一个IP地址,这样方便人们去记忆地址。
|
||||
|
||||
将域名转变为IP地址的过程,我们称之为域名解析。
|
||||
|
||||
## 如何进行域名解析?
|
||||
域名解析在C#中已经封装好了net库,我们只需要学习两个API的使用方法即可。
|
||||
### 同步方法
|
||||
Dns.GetHostEntry(string 域名);
|
||||
该方法通过传入域名,可以返回该域名对应的==主机条目(IPHostEntry)==;
|
||||
### 异步方法
|
||||
Dns.GetHostEntryAsync(string 域名)
|
||||
该方法和上述方法相同,不过他的返回值是Task,需要放在async的方法下面才能执行,最终的实际返回值也是==主机条目(IPHostEntry)==。只是我们需要用task.Result来得到最终的返回值
|
||||
### ==主机条目==
|
||||
主机条目就是域名解析后的返回值,他里面包含了该域名的种种信息,常见的如下
|
||||
- 获取IP地址列表 成员变量:AddressList
|
||||
- 获取主机别名列表 成员变量:Aliases
|
||||
- 获取DNS名称 成员变量:HostName
|
||||
@@ -0,0 +1,37 @@
|
||||
### TryGetValue方法
|
||||
|
||||
“当 `TryGetValue` 方法因为未找到指定的 `key` 而返回 `false` 时,作为 `out` 参数的 `value` 变量,会被强制赋予其类型的默认值(default value),然后这个默认值被‘传出’到调用处的代码。”
|
||||
|
||||
### Animator和哈希值
|
||||
#### **一、问题的根源:为什么字符串比较是坏习惯?**
|
||||
|
||||
在 `Update()` 或其他每帧调用的函数中,使用 `animator.GetCurrentAnimatorStateInfo(0).IsName("状态名")` 或 `animator.SetFloat("参数名", value)` 这样的字符串方法,会带来持续的性能开销。因为字符串比较需要在内存中逐个字符进行匹配,远比整数比较慢。在高频率执行下,这些微小的消耗会累积成可观的性能瓶颈,甚至引发不必要的垃圾回收(GC)。
|
||||
|
||||
#### **二、解决方案:`Animator.StringToHash()`**
|
||||
|
||||
Unity 的标准解决方案是预先将字符串(状态名或参数名)转换成一个唯一的整数ID(哈希值)。这个转换操作 `Animator.StringToHash("你的名字")` 本身有一定开销,所以我们应该在 `Awake()` 或 `Start()` 中**只执行一次**,然后将得到的整数ID存储在一个 `int` 变量中。之后在 `Update()` 里,所有操作都使用这个整数ID,将昂贵的字符串比较替换为速度极快的整数比较。
|
||||
|
||||
#### **三、实践方法:状态(State)与参数(Parameter)**
|
||||
|
||||
- **比较状态:**
|
||||
|
||||
1. 预先计算哈希值:`int attackStateHash = Animator.StringToHash("Attack");`
|
||||
|
||||
2. 在 `Update` 中比较:`if (animator.GetCurrentAnimatorStateInfo(0).shortNameHash == attackStateHash)`
|
||||
|
||||
|
||||
- `shortNameHash` 是 `AnimatorStateInfo` 自带的、代表当前状态名的整数哈希值。
|
||||
|
||||
- **设置参数:**
|
||||
|
||||
1. 预先计算哈希值:`int speedHash = Animator.StringToHash("Speed");`
|
||||
|
||||
2. 在 `Update` 中设置:`animator.SetFloat(speedHash, moveSpeed);`
|
||||
|
||||
|
||||
- 此方法适用于所有参数类型:`SetFloat`, `SetBool`, `SetInteger`, `SetTrigger`。
|
||||
|
||||
|
||||
#### **四、最终结论与最佳实践**
|
||||
|
||||
将所有在**高频函数**中使用的 Animator 字符串,预先转换为哈希值,这不是一个可选的“微优化”,而是编写高性能、专业级代码的**标准规范**。养成在 `Awake()` 中初始化所有动画哈希值的习惯,能显著提升程序运行效率,减少性能毛刺,并让代码更健壮、更易于维护。
|
||||
@@ -0,0 +1 @@
|
||||
#AI
|
||||
@@ -0,0 +1,24 @@
|
||||
#AI
|
||||
#配置
|
||||
``powershell
|
||||
setx ANTHROPIC_API_KEY "YOUR_DASHSCOPE_API_KEY"
|
||||
setx ANTHROPIC_BASE_URL "https://open.bigmodel.cn/api/anthropic"
|
||||
```
|
||||
配置API_KEY和URL
|
||||
可以新打开一个窗口使用一下代码来检查
|
||||
```powershell
|
||||
echo %ANTHROPIC_API_KEY%
|
||||
echo %ANTHROPIC_BASE_URL%
|
||||
```
|
||||
如果配置失败,可以直接去系统环境变量添加
|
||||
//最新方法
|
||||
直接在C:\Users\Administrator\.claude中添加settings.json,在json中配置对应的api,URL,Model即可
|
||||
```
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "8e51b674-209f-4edb-8ef9-2388b1c5ce60",
|
||||
"ANTHROPIC_BASE_URL": "https://ark.cn-beijing.volces.com/api/coding",
|
||||
"ANTHROPIC_MODEL": "ark-code-latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
| 类型 | 符号 | 占用空间 |
|
||||
| ----- | ------- | ----- |
|
||||
| 有符号整形 | sbyte | 1个字节 |
|
||||
| | int | 4个字节 |
|
||||
| | short | 2个字节 |
|
||||
| | long | 8个字节 |
|
||||
| 无符号整形 | byte | 1个字节 |
|
||||
| | uint | 4个字节 |
|
||||
| | ushort | 2个字节 |
|
||||
| | ulong | 8个字节 |
|
||||
| 浮点数 | float | 4个字节 |
|
||||
| | double | 8个字节 |
|
||||
| | decimal | 16个字节 |
|
||||
| 字符 | char | 2个字节 |
|
||||
| 布尔 | bool | 1个字节 |
|
||||
@@ -0,0 +1,220 @@
|
||||
#### 一.转义字符
|
||||
#### 常用的转义字符有:
|
||||
1.斜杠单引号,表示单引号
|
||||
例子 string str="\\'哈哈哈\\' "
|
||||
2.斜杠双引号,表示双引号
|
||||
例子 string str="\\\\'哈哈哈\\\\' "
|
||||
3.斜杠+n表示换行
|
||||
4.双斜杠表示单斜杠
|
||||
#### 不常用转义字符:
|
||||
1.制表符t 相当于按下tab,空4格
|
||||
2.光标退格字符 b,是光标位置向后推一个
|
||||
3.空字符0 无实际效果
|
||||
4.警报音 a 系统发出提示音
|
||||
|
||||
#### 取消转义字符
|
||||
在字符串前面加上@,将所有转义字符按照字面显示
|
||||
|
||||
### 二:类型转换
|
||||
#### 隐式类型转换
|
||||
小转大,不会影响精度,可以自动转换
|
||||
#### 显示类型转换
|
||||
##### 1. 括号强转
|
||||
##### 2.Parse法
|
||||
将string转成目标类型
|
||||
##### 3.Convert法
|
||||
##### 4.其他类型转string ToString()方法
|
||||
|
||||
### 三:枚举
|
||||
枚举是一组命名的整型常量
|
||||
它的本质是整数类型的包装,枚举在编译后会被编译为一个**密封类(sealed class)**,继承自 `System.Enum`(而 `System.Enum` 又继承自 `System.ValueType`),因此枚举是值类型。
|
||||
每个枚举成员本质上是一个**常量整数**,默认从 `0` 开始递增(也可手动指定值)。例如:
|
||||
```
|
||||
public enum Time { yi, er, san }
|
||||
```
|
||||
编译后等价于
|
||||
```
|
||||
public sealed class Time : Enum
|
||||
{
|
||||
public const int yi = 0;
|
||||
public const int er = 1;
|
||||
public const int san = 2;
|
||||
}
|
||||
```
|
||||
### 四:接口
|
||||
常规的接口声明是这样的
|
||||
```
|
||||
interface ICanRun
|
||||
{
|
||||
void Run();
|
||||
}
|
||||
```
|
||||
我们不需要显式的去写访问修饰符,接口默认的是internal,程序集可见
|
||||
内部的成员都是public的,继承了接口的方法也是public.
|
||||
继承了接口的类,他需要实现接口提供的契约方法
|
||||
|
||||
```
|
||||
public class Dog : ICanRun
|
||||
{
|
||||
public void Run()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
以上是我们接口中的常规写法,但是会有一些问题出现.
|
||||
假如有两个接口,他们内部都有一个Run方法
|
||||
```
|
||||
interface ICanFastRun
|
||||
{
|
||||
void Run();
|
||||
}
|
||||
interface ICanSlowRun
|
||||
{
|
||||
void Run();
|
||||
}
|
||||
```
|
||||
那么我们的Dog继承了两个接口,这时候在使用常规的方法去实现接口方法,就会出现一些问题.
|
||||
我们需要新的方法去解决问题.
|
||||
==显式接口实现==
|
||||
我们的Dog在同时继承了两个接口的时候,实现方法应该用显示接口实现
|
||||
```
|
||||
public class Dog:ICanFastRun, ICanSlowRun
|
||||
{
|
||||
void ICanSlowRun.Run()
|
||||
{
|
||||
Console.WriteLine("我可以跑的很慢");
|
||||
}
|
||||
void ICanFastRun.Run()
|
||||
{
|
||||
Console.WriteLine("我可以跑的很快");
|
||||
}
|
||||
}
|
||||
```
|
||||
显示接口实现是非常清晰易懂的,让我们知道这个是那个接口的那个方法,但是他会有一些问题,
|
||||
==我们不能直接在Dog变量里面调用Run方法,需要将Dog强转为接口类型==
|
||||
```
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Dog dog = new Dog();
|
||||
dog.Run(); //会报错
|
||||
}
|
||||
}
|
||||
```
|
||||
这就是麻烦的地方,我们需要对Dog进行类型转换才可以调用对应的Run方法
|
||||
```
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Dog dog = new Dog();
|
||||
((ICanFastRun)dog).Run(); //将dog转换为ICanFastRun类型,可移植性跑得快的Run方法
|
||||
((ICanSlowRun)dog).Run(); //将dog转换为ICanSlowRun类型,可移植性跑得慢的Run方法
|
||||
}
|
||||
}
|
||||
```
|
||||
显示接口实现,从另一层面来讲,也是对方法的一种隐藏,对API的一种清洁.
|
||||
### 五:多态中的类型
|
||||
```
|
||||
public class Animal
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class Dog : Animal
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class Cat : Animal
|
||||
{
|
||||
|
||||
}
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Animal animal = new Animal();
|
||||
Animal dog = new Dog();
|
||||
Animal cat = new Cat();
|
||||
}
|
||||
}
|
||||
```
|
||||
在上述代码当中请问animal,dog,cat他们三个都是什么类型?
|
||||
答案是两个,他们即是Animal又是Dog(Cat),他们两者兼备
|
||||
这是需要区分情况的
|
||||
##### ==在编译时==
|
||||
他们三者都是Animal类型的
|
||||
##### ==在运行时==
|
||||
他们是new 后面的类型
|
||||
dog是Dog类型
|
||||
cat是Cat类型
|
||||
|
||||
|
||||
|
||||
|
||||
### 六:多态中的new关键字
|
||||
new关键字在类当中用来隐藏基类的方法
|
||||
在继承链当中,他会影响运行时多态
|
||||
```
|
||||
public class Animal
|
||||
{
|
||||
public virtual void Eat()
|
||||
{ Console.WriteLine("我正在吃");
|
||||
}
|
||||
}
|
||||
|
||||
public class Dog : Animal
|
||||
{
|
||||
public new void Eat()
|
||||
{
|
||||
Console.WriteLine("狗正在吃");
|
||||
}
|
||||
}
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Animal animal = new Animal();
|
||||
Animal dog1 = new Dog();
|
||||
Dog dog2 = new Dog();
|
||||
animal.Eat();
|
||||
dog1.Eat();
|
||||
dog2.Eat();
|
||||
}
|
||||
}
|
||||
```
|
||||
上面代码的输出结果是
|
||||
```
|
||||
我正在吃
|
||||
我正在吃
|
||||
狗正在吃
|
||||
```
|
||||
关键点在于Animal dog1 = new Dog();
|
||||
dog1究竟输出的是什么呢?
|
||||
答案是"我正在吃"
|
||||
我们要研究为什么他会这样输出
|
||||
```
|
||||
1. 编译阶段:编译器只认 “声明类型”,确定要调用的方法
|
||||
|
||||
dog1的声明类型是Animal(=左边),编译器在编译时会做两件事:
|
||||
|
||||
- 检查Animal类中是否有Eat()方法:发现Animal有public virtual void Eat()(虚拟方法),符合调用条件,编译通过;
|
||||
|
||||
- 记录 “要调用的是Animal类的Eat()方法”:因为Dog类的Eat()用new修饰,编译器会将其视为 “子类新增的独立方法”,而非对Animal.Eat()的重写,所以不会将dog1与Dog.Eat()关联。
|
||||
|
||||
简单说:编译时,编译器认为dog1是Animal类型,只能调用Animal的方法,根本 “看不到”Dog类用new隐藏的Eat()。
|
||||
|
||||
2. 运行阶段:CLR 执行 “编译时确定的方法”
|
||||
|
||||
运行时,dog1的实际类型是Dog(=右边new Dog()),但 CLR 的执行逻辑受new关键字影响:
|
||||
|
||||
- 若子类用override重写:CLR 会优先执行 “实际类型(Dog)的重写方法”(多态生效);
|
||||
|
||||
- 若子类用new隐藏:CLR 会执行 “编译时确定的父类方法(Animal.Eat())”,因为new修饰的方法与父类方法无关联,CLR 不会去子类中找这个 “独立方法”。
|
||||
|
||||
所以,运行时dog1.Eat()最终执行的是Animal类的Eat(),输出 “我正在吃”。
|
||||
```
|
||||
简单的说,new隐藏了方法之后,他在编译时是什么类型,就执行谁的方法.
|
||||
@@ -0,0 +1,11 @@
|
||||
![[deepseek_mermaid_20260204_d63c65.png]]
|
||||
.net平台从最开始是使用的.net framework,它可以跨语言但是不能跨平台,后面第三方提出了mono的解决方案,可以给.net framework用来跨平台,但这是第三方的解决方案。后面微软自己推出了.net core,作为官方的,开源的,跨平台的解决方案。后面在2020年,提出了统一的.net 5,她统一了所有的平台,包括.net frame和.net core。这是.net的生态发展史。
|
||||
下面我们讲讲mono和IL2CPP两种跨平台的方式。
|
||||
mono的工作流程是
|
||||
mono:C#代码->IL代码->CLR运行时JIT计时编译为机器码从而运行。
|
||||
IL2CPP:C#代码->IL代码->C++代码->本地编译C++代码为机器码->运行。
|
||||
区别就在于对于IL代码的处理,IL2CPP是AOT提前编译,提前就处理好代码,最终打包出去的,只有原生机器码,没有任何其他代码。
|
||||
mono的话,打包出去的还存储这IL代码,在运行时会有一个虚拟机去编译IL代码为机器码去运行。
|
||||
还有一个问题:Unity怎么知道我们的游戏需要打包到那个平台,从而调用该平台的C++编译器来编译c++代码让我们可以使用呢?
|
||||
Unity本身并不提供所有的C++编译器,而是我们在打包不同平台的时候,比如Android需要安装NDK,打包windows需要安装vs等等,在这里我们就可以使用这些工具的C++编译器来帮助我们进行编译。所以开发的时候要提前在build里面选好平台
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
3.18日简单学习了一下protobuf的使用,他由3部分组成。
|
||||
## 1.proto数据结构的定义
|
||||
|
||||
我们需要创建proto类型的数据,提前定义好,需要存储的数据的结构和类型。
|
||||
## 2.protoc编译器下载
|
||||
我们需要去protobuf的官方下载protoc编译器,该编译器,可以将我们写好的proto类型,转变成C#代码,让我们可以使用。
|
||||
执行该代码protoc --csharp_out=. player.proto
|
||||
## 3.dll文件的生成与导入
|
||||
我们通过protoc编译器生成的代码,无法直接使用,因为他引用了我们protobuf的许多代码,我们需要给我们的Unity项目,引入这些dll文件。我们需要通过github下载对应protoc编译器版本的protobuf源代码,找到我们对应的语言(csharp)在里面找到对应的工程文件(在src文件夹下面),然后打开工程文件,右键Google.Protobuf生成,就可以在Google.Protobuf/bin/debug里面生成我们需要的所有的dll文件,我们导入Unity的插件Plugins文件夹中即可。
|
||||
@@ -0,0 +1,8 @@
|
||||
1.单例模式(懒汉式 饿汉式)
|
||||
![[Pasted image 20250514200735.png]]
|
||||
2.属性和字段的区别,属性get set里面如何书写
|
||||
3.字典的使用
|
||||
4.继承关系当中virtual和override对方法的影响
|
||||
5.setparent的两个参数,尤其是第二个对UI的影响
|
||||
6.算法:二分法,移除元素
|
||||
7.ref和out的用处和区别
|
||||
@@ -0,0 +1,6 @@
|
||||
### 1. c#当中,只读属性的本质
|
||||
在LoginManager当中,我们封装了loginData,将他设置为一个只读属性
|
||||
public LoginData LoginData => loginData;(这个写法是表达式体属性)
|
||||
照理说我们不能对其进行修改,但是后续发现,我们可以修改LoginData里面的内容
|
||||
这是因为,只读属性,只是确保当前引用不会被改变,也就是栈当中的地址不会发生改变,但是栈指向的堆内存当中的数据可以修改,也就是说==只读属性只是保护了引用不被改变,而不是引用指向的对象内容。==
|
||||
但如果属性封装的是一个值类型,那么因为值类型的数据存储在栈内存当中,所以值类型完全无法修改.
|
||||
@@ -0,0 +1,26 @@
|
||||
1.Unity当中的样条线Spline
|
||||
spline.EvaluatePosition 获得样条线上某个点的位置
|
||||
spline.EvaluateTangent 获得样条线上某个点的切线
|
||||
spline.EvaluateUpVector 获得与切线垂直的一个朝向 z轴
|
||||
Quaternion.LookRotation(x,y)
|
||||
将物体的z轴对齐x参数,y轴对齐y参数.
|
||||
2.场景加载
|
||||
3.资源加载
|
||||
4.Transform
|
||||
transform.position是相对于世界坐标的
|
||||
transform.localposition是相对于父位置的
|
||||
5.抽象类和接口的区别
|
||||
6.类和结构体的区别
|
||||
7.string和stringbuilder的区别
|
||||
8.动画帧事件的添加,他的脚本挂载在Animatior的物体上
|
||||
Animator挂载在哪个物体上,脚本就挂载在哪个物体上,这样才能调用
|
||||
### 9.ref和out的区别
|
||||
- ref在使用之前必须赋值,在内部可以修改,也可以不修改. out在使用之前不需要赋值,在内部必须修改,否则出错. 同时你无法在方法内部,访问out参数的值
|
||||
- ![[Pasted image 20250525175247.png]]
|
||||
所以图中内容会出错,因为他访问了out参数.
|
||||
### 10.单例模式的写法
|
||||
1.在普通脚本当中
|
||||
![[Pasted image 20250525181319.png]]
|
||||
2.在继承mono的脚本当中
|
||||
![[Pasted image 20250525181358.png]]
|
||||
继承自mono的脚本不可以被new出来,因此我们不能和普通脚本一样new,需要在awake当中,将自己赋值给instance;
|
||||
@@ -0,0 +1,44 @@
|
||||
[相机控制]
|
||||
今天学习的是MMO的15节课摄像机
|
||||
通过多层结构控制摄像机的移动缩放,并且适配安卓平台的手指旋转缩放
|
||||
### 第三人称相机控制
|
||||
在新版本基本上是使用cinemamachine来控制相机的移动,在15年的时候并没有这个插件,因此教程使用多层结构来手搓了一个第三人称相机控制器
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Transform的一些知识
|
||||
|
||||
当你在一个 `Transform` 对象上使用 `foreach` 循环时,你实际上是在遍历它在场景层级 (Hierarchy) 中的所有**子对象 (child objects)**。
|
||||
|
||||
可以把 `pellets` 这个 `Transform` 想象成一个父容器,它下面可以挂载很多子物体。`foreach` 循环会逐一访问它的每一个子物体。
|
||||
|
||||
**在你的代码中:**
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
public Transform pellets; // 你会在Inspector中把一个父物体(比如一个空对象)拖到这里,这个父物体下包含了所有的豆子。
|
||||
|
||||
// ...
|
||||
|
||||
public void NewRound()
|
||||
{
|
||||
// 这个循环会遍历 'pellets' 这个Transform下的每一个子物体的Transform组件
|
||||
foreach (Transform pellet in pellets)
|
||||
{
|
||||
// 'pellet' 在每次循环中,都是一个子物体的 Transform
|
||||
// 比如,你可以在这里重新激活每一个豆子
|
||||
pellet.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**总结一下:**
|
||||
|
||||
- **不是数组**:`pellets` 是单个 `Transform` 组件。
|
||||
- **可以遍历**:因为它被设计成可以像集合一样,让你能方便地访问其所有的子物体。
|
||||
- **遍历内容**:循环遍历的是它所有子物体的 `Transform`。
|
||||
|
||||
这是一个非常有用的 Unity 设计,可以让你很方便地管理一组相关的游戏对象(比如所有的敌人、所有的金币、或者你代码里所有的豆子)。
|
||||
@@ -0,0 +1,33 @@
|
||||
### 1.ransform旋转
|
||||
直接修改transform.rotation以及使用方法transform.rotate的区别是什么?
|
||||
修改transform.rotation是直接控制旋转到一个目标量
|
||||
使用方法transform.rotate是控制物体旋转增加一个目标量.
|
||||
比如当前是60,我们的目标量是30,我们修改transform.rotation之后会变成30,但是使用方法会变成90.
|
||||
### 2.坐标系
|
||||
==在不同的canvas设置模式下,以下三种略有不同==
|
||||
1.recttransform.position 在覆盖模式下,就相当于屏幕坐标,左下角是00
|
||||
2.rt.localPosition 是子物体相对于父物体的坐标,他们是根据轴心点来计算的
|
||||
3.rt.anchoredPosition 是自身中心点距离锚点的坐标,也是inspector中的坐标.
|
||||
|
||||
但是如过是在canvas中的摄像机模式下第一种情况会发生变化
|
||||
recttransform.position在摄像机模式下,坐标就是世界坐标系中的坐标.
|
||||
|
||||
在世界模式下,以上内容就相当于是一个游戏物体,等同于transform.
|
||||
|
||||
补充:真实屏幕像素 = anchoredPosition × canvas.scaleFactor
|
||||
所以anchoredPosition=真实屏幕像素/canvas.scaleFactor
|
||||
这和我们的缩放模式息息相关,只有在缩放模式等于我们的参考分辨率的时候,我们的anchoredPosition和我们的像素才是一样的。
|
||||
在分辨率不同的情况下,都是重新计算过的。
|
||||
|
||||
### 3.Mask遮罩
|
||||
遮罩的内容比较简单
|
||||
我们需要在父物体上放置Mask组件,之后==透明区域会遮挡子物体,不透明区域会显示物体.==
|
||||
值得注意的有两点:
|
||||
1.子物体需要勾选Maskable
|
||||
2.父物体如过想显示自身sprite,需要勾选Mask组件的 Show mask Graphic
|
||||
|
||||
### 4.屏幕坐标转UI相对坐标
|
||||
本内容应该同[[#2.坐标系]]相联系.
|
||||
|
||||
### 5.摄像机Clear Flags参数
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#### 1.Toogle Group组件
|
||||
allow switch off 的作用是什么
|
||||
#### 2.Shadow和Outline组件
|
||||
use graphic alpha的作用是什么
|
||||
|
||||
#### 3.Toogle 组件
|
||||
toggle transition的作用是什么
|
||||
|
||||
#### 4.InputField组件
|
||||
placeholder怎么翻译
|
||||
caret怎么翻译
|
||||
custom caret color什么作用
|
||||
selection color什么作用
|
||||
hide mobile input什么作用
|
||||
should activate on select什么作用
|
||||
|
||||
#### 5.Scroll View(Scroll Rect组件)
|
||||
1.Inertia的作用是什么
|
||||
2.Deceleration Rate的作用是什么
|
||||
3.Scroll Sensitivity的作用是什么
|
||||
4.On Value Changed(Vector2)当值改变的时候的这个值是什么?
|
||||
# 延迟函数
|
||||
### 1.invoke 有两个参数,第一个传入string类型的方法名字,第二个参数是延时多久
|
||||
注意事项:
|
||||
1.延迟函数只能调用无参函数
|
||||
2.延迟函数只能调用当前类中的函数
|
||||
想要解决以上问题,只需要包裹一层方法即可.
|
||||
### 2.重复延迟函数 invokerepeating
|
||||
有三个参数,第一个传入string类型的方法名字,第二个参数是延时多久,第三个是之后间隔多久执行.
|
||||
#### 如何取消重复延迟函数
|
||||
cancelInvoke
|
||||
不输入参数则取消所有,输入参数则定向取消某一个.
|
||||
#### 如何判断有延迟函数
|
||||
isInvoking()
|
||||
### 3.对象失活和销毁对延时函数的影响
|
||||
对象失活,脚本依然存在,延时函数可以执行
|
||||
对象摧毁,脚本同样摧毁,延时函数不可以执行.
|
||||
|
||||
#### 4.destroy也可以延时销毁
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
### UGUI打图集
|
||||
打图集的目的是为了减少DC(Draw Call)可以提高性能,打图集之后可以将n次的DC变成一个图集的DC来提高性能.
|
||||
我们需要在Edit-Projecting setting-Editor-Sprite Packer其中有四种模式
|
||||
|
||||
- Sprite Atlas V1 - Enable For Builds
|
||||
- Sprite Atlas V1 - Always Enabled
|
||||
- Sprite Atlas V2 - Enabled
|
||||
- Sprite Atlas V2 - Enabled for builds
|
||||
总的来说可以从两个标准分为两类
|
||||
从版本角度,有V1 V2两种,V2是当前版本最新的,V1是旧版本的图集.
|
||||
从图集制作的角度,分为在builds的时候才可以打图集(Enable For Builds),以及任何时候都可以打图集(Always Enabled).
|
||||
@@ -0,0 +1,485 @@
|
||||
# 一. 射线检测 (Ray Detection)
|
||||
|
||||
## 1. 射线对象 (Ray Object)
|
||||
|
||||
### 1.1 3D世界中的射线 (Ray in a 3D World)
|
||||
|
||||
射线是一条具有**初始点**和**方向**的直线(注意:方向不是终点)。 声明射线时,需要传入初始点和方向。
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
// Ray(Vector3 origin, Vector3 direction)
|
||||
Ray ray = new Ray(origin, direction);
|
||||
|
||||
// 示例:从原点 (0,0,0) 指向右方 (1,0,0) 的射线
|
||||
Ray rayFromOriginToRight = new Ray(Vector3.zero, Vector3.right);
|
||||
```
|
||||
|
||||
### 1.2 摄像机发出的射线 (Ray from Camera)
|
||||
|
||||
这是从屏幕上的一个点(例如鼠标位置)作为起点,沿着摄像机视口方向发出的一条射线。 其初始点即为摄像机位置,方向由屏幕坐标转换而来。
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
// 从主摄像机发出,基于当前鼠标在屏幕上的位置
|
||||
Ray rayFromCamera = Camera.main.ScreenPointToRay(Input.mousePosition);
|
||||
```
|
||||
|
||||
> **注意**: 单独的射线没有实际作用,需要结合射线检测才有意义。
|
||||
|
||||
---
|
||||
|
||||
## 2. 射线检测 (Raycasting)
|
||||
|
||||
### 2.1 最原始的射线检测 (Basic Raycast)
|
||||
|
||||
这种检测方法返回一个布尔值 (`bool`),用于判断射线是否碰撞到任何物体。
|
||||
|
||||
**参数说明:**
|
||||
|
||||
1. `ray`: 发出的射线。
|
||||
2. `maxDistance`: 检测的最大距离。
|
||||
3. `layerMask`: 指定检测的层级 (Layer)。
|
||||
4. `queryTriggerInteraction`: 是否与触发器 (Trigger) 交互。
|
||||
- `UseGlobal`: 使用全局设置。
|
||||
- `Collide`: 检测触发器。
|
||||
- `Ignore`: 忽略触发器 (默认值,如果不填,则使用 `UseGlobal`)。
|
||||
|
||||
<!-- end list -->
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
// 示例:检测名为 "Player" 的层级,最大距离1000,忽略触发器
|
||||
if (Physics.Raycast(ray, 1000f, 1 << LayerMask.NameToLayer("Player"), QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
// 射线碰到了 "Player" 层上的物体
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 获得相交的单个物体的信息 (Getting Information about a Single Hit)
|
||||
|
||||
此方法同样返回 `bool` 值,但能通过 `out` 参数获取碰撞到的单个物体的详细信息。
|
||||
|
||||
**参数说明:**
|
||||
|
||||
1. `ray`: 发出的射线。
|
||||
2. `out RaycastHit hitInfo`: `out` 关键字表示此参数会传出碰撞信息。
|
||||
3. `maxDistance`: 检测的最大距离。
|
||||
4. `layerMask`: 指定检测的层级。
|
||||
5. `queryTriggerInteraction`: 是否与触发器交互。
|
||||
|
||||
<!-- end list -->
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
RaycastHit hit; // 用来存储碰撞信息
|
||||
if (Physics.Raycast(ray, out hit, 1000f, 1 << LayerMask.NameToLayer("Enemy"), QueryTriggerInteraction.Collide))
|
||||
{
|
||||
// 射线碰到了物体,碰撞信息存储在 'hit' 中
|
||||
// hit.collider: 碰到的碰撞体
|
||||
// hit.point: 碰撞点坐标
|
||||
// hit.normal: 碰撞点处的法线向量
|
||||
// hit.distance: 射线起点到碰撞点的距离
|
||||
// GameObject hitObject = hit.collider.gameObject; // 获取碰撞到的游戏对象
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 获得相交的多个物体 (Getting Information about Multiple Hits)
|
||||
|
||||
此方法返回一个 `RaycastHit` 类型的数组。如果射线没有碰撞到任何物体,则数组长度为0。
|
||||
|
||||
**参数说明:**
|
||||
|
||||
1. `ray`: 发出的射线。
|
||||
2. `maxDistance`: 检测的最大距离。
|
||||
3. `layerMask`: 指定检测的层级。
|
||||
4. `queryTriggerInteraction`: 是否与触发器交互。
|
||||
|
||||
<!-- end list -->
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
RaycastHit[] hits;
|
||||
hits = Physics.RaycastAll(ray, 1000f, 1 << LayerMask.NameToLayer("Obstacle"));
|
||||
|
||||
if (hits.Length > 0)
|
||||
{
|
||||
foreach (RaycastHit hit in hits)
|
||||
{
|
||||
// 处理每一个碰撞到的物体
|
||||
// Debug.Log("Hit: " + hit.collider.name);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **`RaycastHit` 结构体**: 在射线检测中非常关键。它记录了大量信息,例如:
|
||||
>
|
||||
> - `collider`: 射线命中的碰撞体,通过它可以获取整个游戏对象。
|
||||
> - `point`: 射线检测到的碰撞点。
|
||||
> - `normal`: 射线与表面相交点的法线向量。
|
||||
> - `distance`: 射线起点到相交点的距离。
|
||||
|
||||
---
|
||||
|
||||
# 二. 范围检测 (Overlap Detection)
|
||||
|
||||
范围检测是在一个固定的区域内,检测其中所有携带碰撞体 (Collider) 的物体。 Unity 中主要有三种范围检测:
|
||||
|
||||
- 盒状范围检测 (`OverlapBox`)
|
||||
- 球形范围检测 (`OverlapSphere`)
|
||||
- 胶囊体范围检测 (`OverlapCapsule`)
|
||||
|
||||
这些方法都存储在 `Physics` 类中,参数大致相同,只是根据形状不同,需要传入不同的点来构造形状。
|
||||
|
||||
## 1. 盒状范围检测 (OverlapBox)
|
||||
|
||||
`Physics.OverlapBox()`
|
||||
|
||||
**参数说明:**
|
||||
|
||||
1. `center`: 立方体的中心点。
|
||||
2. `halfExtents`: 立方体三个轴向的半尺寸 (大小的一半)。
|
||||
3. `orientation`: 立方体的旋转角度 (四元数 `Quaternion`)。
|
||||
4. `layerMask`: **检测指定层级**。
|
||||
5. `queryTriggerInteraction`: 是否忽略触发器。
|
||||
|
||||
**返回值**: 范围内的所有碰撞体 (`Collider[]`) 数组。
|
||||
|
||||
### 重点参数四:检测指定层级 (LayerMask)
|
||||
|
||||
Unity 中的 Layer共有32个 (0-31),这刚好可以用一个32位的二进制数来表示。 `0000 0000 0000 0000 0000 0000 0000 0000`
|
||||
|
||||
**使用 `1 << LayerMask.NameToLayer("LayerName")` 创建层掩码:** `LayerMask.NameToLayer("LayerName")` 函数会返回指定层名称在 Layer 中的索引序号。 将数字 `1` 左移这个索引号的位数,会在32位数字中得到一个全新的数字。例如,如果 "UI" 层是第 5 层 (索引为5): `1 << 5` 结果为 `0000 0000 0000 0000 0000 0000 0010 0000` (第5位为1)
|
||||
|
||||
在这个32位的二进制掩码中,`1` 代表需要检测该层级,`0` 代表不需要。
|
||||
|
||||
**检测多个层级:** 使用按位或 (`|`) 操作可以方便地组合多个层级进行检测。 例如,要检测第0层、第2层和第5层: `0000 0000 0000 0000 0000 0000 0010 0101` (二进制表示)
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
int layerMask = (1 << LayerMask.NameToLayer("第一层")) |
|
||||
(1 << LayerMask.NameToLayer("第二层")) |
|
||||
(1 << LayerMask.NameToLayer("第五层"));
|
||||
// 或者,如果知道层索引:
|
||||
// int layerMask = (1 << 0) | (1 << 2) | (1 << 5);
|
||||
```
|
||||
|
||||
### 位运算在 LayerMask 中的应用
|
||||
|
||||
- **按位与 (`&`)**:
|
||||
|
||||
- 规则:两个位都为 `1` 时,结果才为 `1`。
|
||||
- 用途:检测某个特定层是否存在于当前的层掩码中。
|
||||
- 示例:
|
||||
- 掩码: `0000 ... 0010 0101` (检测0, 2, 5层)
|
||||
- 待检测层 (例如第0层): `0000 ... 0000 0001` (1 << 0)
|
||||
- `(掩码 & 待检测层)` 结果不为0,证明该层在掩码中。
|
||||
- **按位或 (`|`)**:
|
||||
|
||||
- 规则:只要有一个位为 `1`,结果就为 `1`。
|
||||
- 用途:将多个层添加到层掩码中。
|
||||
- **按位异或 (`^`)**:
|
||||
|
||||
- 规则:两个位相同为 `0`,不同为 `1`。
|
||||
- 用途:动态切换某个层级的检测状态(添加或移除)。
|
||||
- 示例:
|
||||
- 原始掩码 (检测第1层和第3层): `0000 ... 1010`
|
||||
- 操作层 (第3层): `0000 ... 1000`
|
||||
- `原始掩码 ^ 操作层` 结果: `0000 ... 0010` (第3层的状态被翻转,现在只检测第1层)
|
||||
|
||||
### Layer 和 LayerMask 的区别是什么?
|
||||
|
||||
- **Layer**: 指的是 Unity 编辑器中为 GameObject 分配的单个层级(例如 "Player", "Enemy", "UI")。每个 GameObject 只能属于一个 Layer。Layer 本身是一个整数索引 (0-31)。
|
||||
- **LayerMask**: 是一个32位的整数,用作位掩码 (bitmask)。它的每一位对应一个 Layer。通过设置 LayerMask 中的特定位为1,可以指定射线检测、范围检测等操作应该作用于哪些 Layer。一个 LayerMask 可以同时代表多个 Layer。
|
||||
|
||||
---
|
||||
|
||||
# 三. RPG游戏学习 (RPG Game Study)
|
||||
|
||||
## 1. 导航系统 (Navigation System)
|
||||
|
||||
- (内容待补充)
|
||||
|
||||
## 2. 鼠标点击, 角色移动 (Mouse Click, Character Movement)
|
||||
|
||||
- (内容待补充)
|
||||
|
||||
---
|
||||
|
||||
# 四. 对象池技术 (Object Pooling)
|
||||
|
||||
目前构造的对象池比较简单。这个对象池是一个单例类。我们需要明确:
|
||||
|
||||
- 对象池存储的物体是什么类型。
|
||||
- 对象池存储的物体初始个数是多少。
|
||||
|
||||
然后构造一个队列 (Queue) 来管理这些对象。
|
||||
|
||||
一个典型的对象池实现:
|
||||
|
||||
1. 在 `Awake()` 方法中完成单例类的构造。
|
||||
2. 实现三个核心方法:
|
||||
- **初始化填充对象池**: 在开始时创建并存储一定数量的对象。
|
||||
- **从对象池中获取对象**: 当需要对象时,从池中取出一个。
|
||||
- **将对象返回对象池**: 当对象不再使用时,将其返还给池中以备复用。
|
||||
|
||||
---
|
||||
|
||||
# 五. LineRenderer (画线组件)
|
||||
|
||||
`LineRenderer` 组件允许在 Unity 场景中绘制线段。它主要包含编辑器设置和代码控制两大部分。代码控制是重点,通常在脚本中动态实现。
|
||||
|
||||
## 1. 编辑器部分 (Editor Properties)
|
||||
|
||||
_(图片引用)_
|
||||
|
||||
1. **Loop**: 线段的开头和结束是否闭合形成循环。
|
||||
2. **Positions**: 控制线段的顶点个数和每个顶点的坐标。
|
||||
3. **Width**: 线段的粗细 (可以是一个曲线,使粗细沿长度变化)。
|
||||
4. **Color**: 线段的颜色 (可以使用渐变色)。
|
||||
5. **Corner Vertices**: 角顶点个数。数值越大,线段转角处越圆润。
|
||||
6. **End Cap Vertices**: 末端顶点个数。数值越大,线段末端越圆润。
|
||||
7. **Alignment**: 对齐方式 (View, TransformZ)。
|
||||
8. **Texture Mode**: 纹理模式 (Stretch, Tile, DistributePerSegment, Shape)。
|
||||
9. **Generate Lighting Data**: 是否生成光照数据,即光照是否对线段产生影响。
|
||||
10. **Use World Space**: 是否使用世界坐标。
|
||||
- 若勾选,移动 LineRenderer 所在的 GameObject 时,线段本身不会随之移动。
|
||||
- 若不勾选 (使用本地坐标),线段会随 GameObject 移动。
|
||||
|
||||
## 2. 代码部分 (Scripting)
|
||||
|
||||
首先,需要获取 `LineRenderer` 组件的引用。
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
LineRenderer lineRenderer;
|
||||
|
||||
void Start()
|
||||
{
|
||||
lineRenderer = GetComponent<LineRenderer>();
|
||||
}
|
||||
```
|
||||
|
||||
**常用代码操作:**
|
||||
|
||||
1. **首尾相连 (Looping)**:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
lineRenderer.loop = true; // 或者 false
|
||||
```
|
||||
|
||||
2. **设置开始和结束宽度**:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
lineRenderer.startWidth = 0.1f;
|
||||
lineRenderer.endWidth = 0.5f;
|
||||
// 也可以使用曲线来控制宽度
|
||||
// lineRenderer.widthCurve = new AnimationCurve(new Keyframe(0, 0.1f), new Keyframe(1, 0.5f));
|
||||
```
|
||||
|
||||
3. **设置开始和结束颜色**:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
lineRenderer.startColor = Color.red;
|
||||
lineRenderer.endColor = Color.blue;
|
||||
// 也可以使用 Gradient 来控制颜色
|
||||
// Gradient gradient = new Gradient();
|
||||
// gradient.SetKeys(
|
||||
// new GradientColorKey[] { new GradientColorKey(Color.red, 0.0f), new GradientColorKey(Color.blue, 1.0f) },
|
||||
// new GradientAlphaKey[] { new GradientAlphaKey(1.0f, 0.0f), new GradientAlphaKey(1.0f, 1.0f) }
|
||||
// );
|
||||
// lineRenderer.colorGradient = gradient;
|
||||
```
|
||||
|
||||
4. **设置材质 (Material)**:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
public Material lineMaterial; // 在 Inspector 中指定
|
||||
// ...
|
||||
lineRenderer.material = lineMaterial;
|
||||
```
|
||||
|
||||
5. **设置顶点 (Positions)**:
|
||||
|
||||
- 设置顶点数量:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
lineRenderer.positionCount = 2; // 例如,一条直线需要2个点
|
||||
```
|
||||
|
||||
- 设置单个顶点坐标:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
// lineRenderer.SetPosition(int index, Vector3 position);
|
||||
lineRenderer.SetPosition(0, new Vector3(0, 0, 0));
|
||||
lineRenderer.SetPosition(1, new Vector3(1, 1, 0));
|
||||
```
|
||||
|
||||
- 通过数组一次性设置所有顶点坐标:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
Vector3[] positions = new Vector3[] {
|
||||
new Vector3(0, 0, 0),
|
||||
new Vector3(1, 1, 0),
|
||||
new Vector3(2, 0, 0)
|
||||
};
|
||||
lineRenderer.SetPositions(positions);
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
# 六. 四元数 (Quaternions)
|
||||
|
||||
- (内容待补充,用于表示旋转)
|
||||
|
||||
---
|
||||
|
||||
# 七. 组件和物体的启用和禁用 (Component and GameObject Enabling/Disabling)
|
||||
|
||||
## 1. GameObject 的启用和禁用
|
||||
|
||||
### 1.1 激活 GameObject
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
// 'this' 指向当前脚本所在的 GameObject
|
||||
this.gameObject.SetActive(true);
|
||||
```
|
||||
|
||||
### 1.2 禁用 GameObject
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
this.gameObject.SetActive(false);
|
||||
```
|
||||
|
||||
### 1.3 查看 GameObject 状态
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
// 自身是否被设置为激活 (不受父物体影响)
|
||||
bool isActiveSelf = gameObject.activeSelf;
|
||||
|
||||
// 在层级视图中实际的激活状态 (受父物体影响)
|
||||
bool isActiveInHierarchy = gameObject.activeInHierarchy;
|
||||
```
|
||||
|
||||
### 1.4 `activeSelf` 和 `activeInHierarchy` 的区别
|
||||
|
||||
- `gameObject.activeSelf`: 表示该 GameObject 自身是否被设置为激活状态。如果它被设置为 `false`,即使其所有父对象都激活,它依然是非激活的。
|
||||
- `gameObject.activeInHierarchy`: 表示该 GameObject 在场景中是否真实处于激活状态。如果它自身 `activeSelf` 为 `true`,但其任何一个父对象 `activeSelf` 为 `false` (导致父对象 `activeInHierarchy` 为 `false`),那么该 GameObject 的 `activeInHierarchy` 也会是 `false`。
|
||||
|
||||
## 2. 组件的启用和禁用
|
||||
|
||||
### 2.1 激活组件
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
// 对于当前脚本组件本身
|
||||
this.enabled = true;
|
||||
|
||||
// 对于其他组件 (例如 Collider),需要先获取其引用
|
||||
Collider myCollider = this.GetComponent<Collider>();
|
||||
if (myCollider != null)
|
||||
{
|
||||
myCollider.enabled = true;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 禁用组件
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
// 对于当前脚本组件本身
|
||||
this.enabled = false;
|
||||
|
||||
// 对于其他组件
|
||||
Collider myCollider = GetComponent<Collider>();
|
||||
if (myCollider != null)
|
||||
{
|
||||
myCollider.enabled = false;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 检查组件的启用状态
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
// 当前脚本组件的状态
|
||||
bool isScriptEnabled = this.enabled;
|
||||
|
||||
// 其他组件的状态
|
||||
Collider myCollider = GetComponent<Collider>();
|
||||
bool isColliderEnabled = false;
|
||||
if (myCollider != null)
|
||||
{
|
||||
isColliderEnabled = myCollider.enabled;
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 相关的生命周期事件 (Lifecycle Events)
|
||||
|
||||
### 3.1 组件相关的生命周期方法:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
void OnEnable()
|
||||
{
|
||||
// 当组件被启用时调用。
|
||||
// 这也包括其所在的 GameObject 从非激活状态变为激活状态时。
|
||||
// 注意: 如果 GameObject 首次激活,OnEnable 会在 Awake 和 Start 之后(对于脚本而言)或之前(对于某些内置组件)被调用,
|
||||
// 具体取决于脚本执行顺序和组件类型。通常在 Awake 之后,Start 之前或之后。
|
||||
// 严格来说,是 Awake -> OnEnable -> Start
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
// 当组件被禁用时调用。
|
||||
// 这也包括其所在的 GameObject 从激活状态变为非激活状态时。
|
||||
// 不会触发 OnDestroy()。
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 GameObject 相关的生命周期影响:
|
||||
|
||||
- 调用 `gameObject.SetActive(true)`:
|
||||
- 会触发该 GameObject 及其所有激活的子物体上所有激活组件的 `OnEnable()` 方法。
|
||||
- 如果该 GameObject 是首次被激活 (之前从未激活过或被实例化后首次激活),还会先调用其上所有组件的 `Awake()`,然后是 `OnEnable()`,接着是 `Start()`。
|
||||
- 调用 `gameObject.SetActive(false)`:
|
||||
- 会触发该 GameObject 及其所有激活的子物体上所有激活组件的 `OnDisable()` 方法。
|
||||
- **不会**触发 `OnDestroy()`。
|
||||
|
||||
---
|
||||
|
||||
# 八. MonoBehaviour 中的重要内容和 GameObject 的重要内容 (Key Aspects of MonoBehaviour and GameObject)
|
||||
|
||||
- (内容待补充)
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,49 @@
|
||||
![[Pasted image 20241218120120.png]]
|
||||
|
||||
RigidBody组件是Unity引擎内置的物理组件,他的存在可以使得GameObject拥有物理属性.但没有物理形状,形状有Collider控制
|
||||
从上往下以此讲解该组件的全部属性.
|
||||
### Mass
|
||||
物体的质量,不影响下落速度
|
||||
### Drag和AngularDrag
|
||||
这两个均为阻力,线性阻力和角阻力
|
||||
### UseGravity
|
||||
是否启用重力,启用之后物体收到重力影响
|
||||
### IsKinematic
|
||||
是否启用运动学,启用之后物体不受到物理引擎的运动学效果
|
||||
例如:
|
||||
1.不会与其他物体产生碰撞反应
|
||||
2.不会有重力,力,冲量的影响.
|
||||
但是可以通过脚本来驱动该物体的运行.
|
||||
### InterPolate
|
||||
![[Pasted image 20241219223339.png]]
|
||||
展开后为上图所示
|
||||
该选项的作用是是否启用插值可以选择提前插值,也可以选择延后插值,
|
||||
具体效果可以修复游戏在游玩时候的卡顿过程.
|
||||
注意:有时候游戏在引擎当中游玩不会有卡顿,在Built之后会有卡顿.
|
||||
### Collision Detection 碰撞检测
|
||||
|
||||
![[Pasted image 20241219223512.png]]
|
||||
一共有4种选择,他们分别是离散检测,持续检测,动态持续检测,推断性检测
|
||||
#### Discrete (离散检测)
|
||||
该检测的频率与FixUpdata相同,因此适用于大部分的场合
|
||||
#### Continuous(连续模式)
|
||||
==一个动态物体和一个静态物体(没有刚体的碰撞器)之间可以使用==
|
||||
该模式修复了离散模式在物体速度过快时出现的穿墙现象,优点显著
|
||||
但是缺点同样显著,其一是系统开销大
|
||||
缺点之二是该方法是通过撞击时间 (TOI) 算法,通过扫掠对象的前向轨迹来计算对象的潜在碰撞(采用对象的当前速度).因此会忽视物体的角运动,导致物体可能在侧边会出现穿墙现象.
|
||||
#### Continuous Dynamic(连续动态模式)
|
||||
类似于Continuous,但是==支持高速移动的物体和其他动态物体的检测==,系统开销最大.
|
||||
#### Continuous Speculative(连续推测模式)
|
||||
系统开销介于离散模式和连续模式之间.
|
||||
同样基于预测,可以防止物体穿墙现象.
|
||||
可以避免Continuous(连续模式)中忽视角运动侧边穿墙的问题.
|
||||
具体原理:https://docs.unity.cn/cn/2022.3/Manual/ContinuousCollisionDetection.html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 182 KiB |
@@ -0,0 +1,128 @@
|
||||
对于碰撞器而言,基本的碰撞器形状,碰撞器的作用不必多谈,这里着重强调的是碰撞器和触发器,以及他们碰撞和触发的三种状态.
|
||||
### Enter Stay Exit
|
||||
以上三种状态表示了物体碰撞刚刚进入的一帧,物体持续碰撞,和物体离开碰撞的一帧
|
||||
他们分别对应了代码当中的
|
||||
OnCollisionEnter
|
||||
OnCollisionStay
|
||||
OnCollisionExit
|
||||
以及触发器的
|
||||
OnTriggerEnter
|
||||
OnTriggerStay
|
||||
OnTriggerExit
|
||||
他们的用法相同,区别在于方法传入的参数不同
|
||||
碰撞器提供的参数类型是Collision
|
||||
触发器提供的参数类型是Collider
|
||||
### 筛选
|
||||
我们需要在碰撞器和触发器内部进行许多逻辑上的处理,这里有一个关键是如何筛选物体
|
||||
比如NPC和玩家同时触发开门触发器,我们只希望玩家触发时才会开门,需要进行筛选.
|
||||
常见的筛选方法有2种: 签名. 层
|
||||
#### 签名
|
||||
由于为我们提供了参数,我们可以获取到触发的物体,所以我们可以使用签名进行筛选
|
||||
譬如:
|
||||
`if (collision.gameObject.tag==("Player"))`
|
||||
`{`
|
||||
|
||||
`}`
|
||||
`if (collision.gameObject.CompareTag("Tag"))`
|
||||
`{`
|
||||
|
||||
`}`
|
||||
二种方式均可,其中第二种方式的效率更高,因为:
|
||||
Unity将标签单独储存在一个数据结构当中,CompareTag方法可以直接访问该数据结构效率更高.
|
||||
#### 层
|
||||
在Unity当中,层被储存为整数,因此使用层进行筛选的效率比Tag高.
|
||||
层的筛选也需要分为筛选特定的单层,和多层
|
||||
比如你需要玩家单独可以开门,那么就可以使用单层筛选
|
||||
但是如过你想要多个物体都可以开门,那么就需要多层
|
||||
#### 单层检测
|
||||
![[Pasted image 20241219231058.png]]
|
||||
#### 多层检测
|
||||
|
||||
![[Pasted image 20241219231128.png]]
|
||||
|
||||
### **1. 关键概念**
|
||||
|
||||
#### **Layer 和 LayerMask**
|
||||
|
||||
- **Layer(层)**: Unity 中的每个 `GameObject` 都可以设置一个 `Layer`,用于分组、筛选或逻辑处理。每个层的编号是从 `0` 到 `31` 的整数。
|
||||
|
||||
- **LayerMask(层掩码)**: `LayerMask` 是一个 32 位的整型数,每一位(bit)对应一个 Layer。如果某一位是 `1`,则表示该层被包含在掩码中;如果是 `0`,则表示不包含。
|
||||
|
||||
例如:
|
||||
|
||||
- 如果 `LayerMask` 的值是 `5`(即二进制 `00000000 00000000 00000000 00000101`),表示包含第 0 层和第 2 层。
|
||||
- 具体映射:
|
||||
- 第 0 位 = 1 → 包含第 0 层
|
||||
- 第 1 位 = 0 → 不包含第 1 层
|
||||
- 第 2 位 = 1 → 包含第 2 层
|
||||
|
||||
---
|
||||
|
||||
### **2. 拆解逻辑**
|
||||
|
||||
#### **(1) `collision.gameObject.layer`**
|
||||
|
||||
- 获取发生碰撞的对象的层编号(一个整数,例如 `0`、`1`、`2` 等)。
|
||||
|
||||
#### **(2) `(1 << collision.gameObject.layer)`**
|
||||
|
||||
- **位移操作 `<<`**: `1 << x` 表示将 `1` 左移 `x` 位。例如:
|
||||
|
||||
- `1 << 0` → `00000000 00000000 00000000 00000001` (表示第 0 层)
|
||||
- `1 << 2` → `00000000 00000000 00000000 00000100` (表示第 2 层)
|
||||
|
||||
**结果:** 这个操作生成一个位掩码,只有对应层的位置是 `1`,其余位置是 `0`。
|
||||
|
||||
|
||||
#### **(3) `targetLayers | (1 << collision.gameObject.layer)`**
|
||||
|
||||
- **按位或操作 `|`**: 将 `targetLayers` 和生成的位掩码进行按位或运算。
|
||||
|
||||
- 如果 `targetLayers` 中已经包含对应的层(对应位置是 `1`),运算结果不变。
|
||||
- 如果 `targetLayers` 中不包含对应的层(对应位置是 `0`),运算结果会将该位置变为 `1`。
|
||||
|
||||
**例子:**
|
||||
|
||||
- `targetLayers = 00000000 00000000 00000000 00000101` (包含第 0 层和第 2 层)
|
||||
- `collision.gameObject.layer = 1`
|
||||
- `1 << 1 = 00000000 00000000 00000000 00000010` (表示第 1 层)
|
||||
- 运算结果:`00000000 00000000 00000000 00000111` (包含第 0、1 和 2 层)
|
||||
|
||||
#### **(4) 比较:`targetLayers == ...`**
|
||||
|
||||
- 比较 `targetLayers` 和运算结果是否相等。
|
||||
- 如果相等,说明碰撞对象的 `Layer` 已经在 `targetLayers` 中。
|
||||
- 如果不相等,说明碰撞对象的 `Layer` 不在 `targetLayers` 中。
|
||||
|
||||
---
|
||||
|
||||
### **3. 具体判断逻辑**
|
||||
|
||||
代码的核心逻辑是:
|
||||
|
||||
1. 通过 `1 << collision.gameObject.layer` 获取碰撞对象的层对应的位掩码。
|
||||
2. 用 `targetLayers | ...` 检查碰撞对象的层是否在 `targetLayers` 中。
|
||||
3. 如果运算结果和 `targetLayers` 相等,说明碰撞对象的层已经被包含。
|
||||
|
||||
**简化版本:** `(targetLayers | (1 << collision.gameObject.layer))` 实际上就是在“试探性”地将碰撞对象的层加入 `targetLayers`,然后检查是否变化。如果没有变化,说明碰撞对象的层已经在 `targetLayers` 中。
|
||||
|
||||
---
|
||||
|
||||
### **4. 更直观的判断方式**
|
||||
|
||||
可以使用 `LayerMask` 的内置方法 `LayerMask.Contains`(在较新 Unity 版本中),或者稍微改写代码,提升可读性:
|
||||
|
||||
#### 替代写法:
|
||||
|
||||
csharp
|
||||
|
||||
复制代码
|
||||
|
||||
`if (((1 << collision.gameObject.layer) & targetLayers) != 0) { // 进行内部逻辑 }`
|
||||
|
||||
#### **逻辑解析:**
|
||||
|
||||
- `(1 << collision.gameObject.layer)` 生成碰撞对象层的位掩码。
|
||||
- `&`(按位与)检测目标层掩码是否包含该层。
|
||||
- 如果结果不为 `0`,说明目标层掩码包含碰撞对象的层。
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
void OnReceive(byte[] data, int length)
|
||||
{
|
||||
// 把收到的数据追加到缓存末尾
|
||||
Array.Copy(data, 0, cacheBuffer, cacheCount, length);
|
||||
cacheCount += length;
|
||||
|
||||
// 循环处理缓存中的完整消息
|
||||
while (true)
|
||||
{
|
||||
// 不足4字节,说明长度头还没收全
|
||||
if (cacheCount < 4)
|
||||
break;
|
||||
|
||||
// 取出前4字节的消息长度
|
||||
int msgLength = BitConverter.ToInt32(cacheBuffer, 0);
|
||||
|
||||
// 检查是否收全整个消息
|
||||
if (cacheCount - 4 < msgLength)
|
||||
break;
|
||||
|
||||
// 提取消息体
|
||||
byte[] msgBody = new byte[msgLength];
|
||||
Array.Copy(cacheBuffer, 4, msgBody, 0, msgLength);
|
||||
|
||||
// ---- 处理消息 ----
|
||||
ProcessMessage(msgBody);
|
||||
|
||||
// 将剩余数据前移
|
||||
int remain = cacheCount - 4 - msgLength;
|
||||
Array.Copy(cacheBuffer, 4 + msgLength, cacheBuffer, 0, remain);
|
||||
cacheCount = remain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//清除
|
||||
ClearPieceAt(piece);
|
||||
yield return new WaitForSeconds(0.25f);
|
||||
//塌陷
|
||||
movingPieces=CollapseColumn(piece);
|
||||
|
||||
三消游戏过程中遇到上述代码,我们在前面通过ClearPieceAt清除了piece列表中每一个piece对应的gameobject.
|
||||
但是在塌陷代码中依旧使用piece其中的坐标,对塌陷进行处理.
|
||||
|
||||
之所以塌陷还可以使用piece,而没有报错的原因是
|
||||
@@ -0,0 +1,101 @@
|
||||
### **Unity 场景物体查找API核心笔记**
|
||||
|
||||
在Unity中,动态查找场景中的游戏对象(GameObject)或组件(Component)是一项基本操作。不同的API适用于不同的场景,理解它们的区别和性能开销至关重要。
|
||||
|
||||
#### **一、 全局场景查找 (Scene-Wide Search)**
|
||||
|
||||
这类API会遍历整个场景的层级树来寻找匹配的对象。**它们的共同点是性能开销较大,绝对禁止在 `Update()` 或 `FixedUpdate()` 等高频函数中每帧调用。** 最适合在 `Awake()` 或 `Start()` 中用于初始化引用。
|
||||
|
||||
1. **按名称查找:`GameObject.Find()`**
|
||||
|
||||
- **API:** `public static GameObject Find(string name);`
|
||||
|
||||
- **作用:** 根据物体的**确切名称**查找一个**激活的(active)**游戏对象。如果存在多个同名对象,它会返回哪一个是不确定的。
|
||||
|
||||
- **返回:** `GameObject`。如果找不到则返回 `null`。
|
||||
|
||||
- **笔记:** 这是最不推荐的查找方式之一。因为它依赖于硬编码的字符串,当场景中物体改名时代码就会失效。性能非常差,应尽量避免使用。
|
||||
|
||||
2. **按标签查找:`FindWithTag()` / `FindGameObjectsWithTag()`**
|
||||
|
||||
- **API (单个):** `public static GameObject FindWithTag(string tag);`
|
||||
|
||||
- **API (多个):** `public static GameObject[] FindGameObjectsWithTag(string tag);`
|
||||
|
||||
- **作用:** 根据标签(Tag)查找一个或所有激活的游戏对象。标签需要在Inspector中预先为GameObject设置。
|
||||
|
||||
- **返回:** 单个返回 `GameObject`,多个返回 `GameObject[]` 数组。
|
||||
|
||||
- **笔记:** 比按名字查找要好,因为它不依赖于具体名称,更灵活。但本质上仍然需要遍历场景,性能开销依然很大。
|
||||
|
||||
3. **按组件类型查找:`FindObjectOfType()` / `FindObjectsOfType()` (常用)**
|
||||
|
||||
- **API (单个):** `public static T FindObjectOfType<T>() where T : Object;`
|
||||
|
||||
- **API (多个):** `public static T[] FindObjectsOfType<T>() where T : Object;`
|
||||
|
||||
- **作用:** 查找场景中挂载了特定组件 `T` 的一个或所有激活的对象。`T` 可以是任何继承自 `Component` 的脚本或Unity内置组件(如 `Camera`, `Light`)。
|
||||
|
||||
- **返回:** 单个返回组件 `T` 的引用,多个返回 `T[]` 数组。
|
||||
|
||||
- **笔记:** 这是最常用和推荐的**全局查找**方法。它不依赖名称或标签,而是直接关联代码逻辑(组件类型),更加健壮。但同样,性能开销大,**仅限初始化时使用**。
|
||||
|
||||
- **现代化替代方案:** Unity 推荐使用 `FindAnyObjectByType<T>()` 和 `FindObjectsByType<T>(...)` 作为更新、性能更好的替代品。
|
||||
|
||||
|
||||
#### **二、 局部层级查找 (Hierarchy Search)**
|
||||
|
||||
这类API只在当前GameObject的子级或父级中进行查找,范围小,**性能远高于全局查找**。
|
||||
|
||||
1. **在自身上查找组件:`GetComponent<T>()`**
|
||||
|
||||
- **API:** `public T GetComponent<T>();`
|
||||
|
||||
- **作用:** 获取挂载在**同一个**游戏对象上的组件 `T`。
|
||||
|
||||
- **笔记:** 这是最常用、最高效的获取自身组件的方法。
|
||||
|
||||
2. **在子级中查找:`transform.Find()` / `GetComponentInChildren<T>()`**
|
||||
|
||||
- **API (按名找子物体):** `public Transform transform.Find(string name);`
|
||||
|
||||
- **API (按组件找子物体):** `public T GetComponentInChildren<T>();`
|
||||
|
||||
- **作用:**
|
||||
|
||||
- `transform.Find()`: 根据名称查找一个**直接子级**的 `Transform`。注意,它不会递归查找孙子级。
|
||||
|
||||
- `GetComponentInChildren<T>()`: 查找自身或其**所有子级**(包括孙子级等)中第一个挂载了组件 `T` 的对象。
|
||||
|
||||
- **笔记:** `GetComponentInChildren` 非常适合用来获取预制体(Prefab)内部的某个部件,例如获取枪械模型上的“枪口特效”组件。
|
||||
|
||||
3. **在父级中查找:`GetComponentInParent<T>()`**
|
||||
|
||||
- **API:** `public T GetComponentInParent<T>();`
|
||||
|
||||
- **作用:** 查找自身或其**所有父级**中第一个挂载了组件 `T` 的对象。
|
||||
|
||||
- **笔记:** 常用于UI或模块化设计。例如,一个按钮可以向上查找到它所属的那个“根面板”控制器脚本。
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **三、 性能与最佳实践总结 (至关重要)**
|
||||
|
||||
| 方法 | 查找范围 | 性能 | 推荐用法 |
|
||||
| ------------------------ | -------- | ----------- | ------------------------------------------------------------------- |
|
||||
| **公开变量引用** | 无 (手动指定) | **极高 (最佳)** | **首选方案!** 在脚本中声明 `public GameObject myObject;`,然后在Inspector中手动拖拽赋值。 |
|
||||
| `GetComponent<T>` | 自身 | 非常高 | 在 `Awake`/`Start` 中获取自身其他组件。 |
|
||||
| `GetComponentInChildren` | 自身及所有子级 | 较高 | 初始化时获取Prefab内部的固定部件。 |
|
||||
| `GetComponentInParent` | 自身及所有父级 | 较高 | 模块化组件向上查找控制器或根对象。 |
|
||||
| `FindObjectsOfType<T>` | 整个场景 | **很低** | **仅限**在管理器类的 `Awake` 中,用于查找并注册场景中所有特定类型的对象。 |
|
||||
| `GameObject.Find()` | 整个场景 | **极低 (最差)** | **强烈不推荐**,仅用于快速原型或调试,正式项目中应被替换。 |
|
||||
#### **核心原则笔记:**
|
||||
|
||||
1. **首选“拖拽引用”:** 在脚本中声明一个 `public` 或 `[SerializeField] private` 变量,然后在Unity编辑器里手动将场景中的物体拖拽到该变量上。这是**零开销**、最安全、最高效的方式。
|
||||
|
||||
2. **“Find”仅用于初始化:** 所有全局查找 (`Find`, `FindObjectOfType` 等) 都应该只在 `Awake()` 或 `Start()` 函数中调用**一次**,并将结果缓存到一个私有变量中,供后续使用。
|
||||
|
||||
3. **杜绝在Update中使用Find:** **永远不要**在 `Update()`, `FixedUpdate()`, `LateUpdate()` 中直接调用任何全局查找API。这是导致游戏卡顿的常见原因之一。
|
||||
|
||||
4. **善用局部查找:** 当物体关系固定时(如Prefab内部),优先使用 `GetComponentInChildren` 或 `transform.Find`,它们的性能远好于全局查找。
|
||||
@@ -0,0 +1,178 @@
|
||||
# C# 对象的序列化与反序列化
|
||||
|
||||
## 1. 什么是序列化?
|
||||
|
||||
**序列化(Serialization)** 是指将对象的状态信息转换为可以存储或传输的形式(例如字节流、文件、内存数据)的过程。
|
||||
反之,**反序列化(Deserialization)** 则是将存储或传输的序列化数据还原为对象的过程。
|
||||
|
||||
在 C# 中,最常用的方式是通过 `BinaryFormatter` 类来完成二进制序列化与反序列化。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心类与方法
|
||||
|
||||
- **BinaryFormatter.Serialize(Stream, object)**
|
||||
将对象序列化到指定的流中。
|
||||
|
||||
- **BinaryFormatter.Deserialize(Stream)**
|
||||
从指定的流中反序列化对象。
|
||||
|
||||
|
||||
> ⚠️ 注意:从 .NET 5 开始,官方已经不推荐使用 `BinaryFormatter`(存在安全风险),更推荐 `System.Text.Json` 或 `XmlSerializer` 等。但在学习和理解基础时,仍可使用。
|
||||
|
||||
---
|
||||
|
||||
## 3. 序列化方式
|
||||
|
||||
### 3.1 文件流方式
|
||||
|
||||
通过 **文件流(FileStream)** 将对象直接保存到文件中。
|
||||
|
||||
```
|
||||
csharp
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
|
||||
[Serializable] // 必须标记为可序列化
|
||||
public class Person
|
||||
{
|
||||
public string Name;
|
||||
public int Age;
|
||||
}
|
||||
|
||||
class Program
|
||||
{
|
||||
static void Main()
|
||||
{
|
||||
Person p = new Person { Name = "Tom", Age = 25 };
|
||||
|
||||
// 序列化到文件
|
||||
using (FileStream fs = new FileStream("person.dat", FileMode.Create))
|
||||
{
|
||||
BinaryFormatter formatter = new BinaryFormatter();
|
||||
formatter.Serialize(fs, p);
|
||||
}
|
||||
|
||||
Console.WriteLine("对象已序列化到文件 person.dat");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
### 3.2 内存流方式
|
||||
|
||||
通过 **内存流(MemoryStream)** 将对象序列化成字节数组,便于网络传输或自定义存储。
|
||||
|
||||
```
|
||||
csharp
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
|
||||
[Serializable]
|
||||
public class Person
|
||||
{
|
||||
public string Name;
|
||||
public int Age;
|
||||
}
|
||||
|
||||
class Program
|
||||
{
|
||||
static void Main()
|
||||
{
|
||||
Person p = new Person { Name = "Alice", Age = 30 };
|
||||
byte[] buffer;
|
||||
|
||||
// 序列化到内存流
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
{
|
||||
BinaryFormatter formatter = new BinaryFormatter();
|
||||
formatter.Serialize(ms, p);
|
||||
|
||||
buffer = ms.ToArray(); // 获取字节数组
|
||||
}
|
||||
|
||||
// 可以将字节数组写入文件或通过网络传输
|
||||
File.WriteAllBytes("person_bytes.dat", buffer);
|
||||
|
||||
Console.WriteLine("对象已序列化为字节数组并写入 person_bytes.dat");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
## 4. 反序列化
|
||||
|
||||
### 4.1 从文件反序列化
|
||||
|
||||
```
|
||||
csharp
|
||||
|
||||
using (FileStream fs = new FileStream("person.dat", FileMode.Open))
|
||||
{
|
||||
BinaryFormatter formatter = new BinaryFormatter();
|
||||
Person p = (Person)formatter.Deserialize(fs);
|
||||
|
||||
Console.WriteLine($"Name: {p.Name}, Age: {p.Age}");
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 从字节数组反序列化
|
||||
|
||||
```
|
||||
csharp
|
||||
|
||||
byte[] buffer = File.ReadAllBytes("person_bytes.dat");
|
||||
|
||||
using (MemoryStream ms = new MemoryStream(buffer))
|
||||
{
|
||||
BinaryFormatter formatter = new BinaryFormatter();
|
||||
Person p = (Person)formatter.Deserialize(ms);
|
||||
|
||||
Console.WriteLine($"Name: {p.Name}, Age: {p.Age}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
- **必须添加 `[Serializable]` 特性**
|
||||
否则对象无法被序列化。
|
||||
|
||||
- **字段级别的控制**
|
||||
如果某些字段不想被序列化,可以使用 `[NonSerialized]` 特性修饰。
|
||||
|
||||
```
|
||||
csharp
|
||||
|
||||
[NonSerialized]
|
||||
private string password;
|
||||
|
||||
```
|
||||
|
||||
- **安全问题**
|
||||
`BinaryFormatter` 在反序列化时可能存在安全漏洞,不适合用于处理不受信任的数据。
|
||||
|
||||
- **替代方案**
|
||||
|
||||
- 文本序列化(如 `JsonSerializer`、`XmlSerializer`)
|
||||
|
||||
- 跨平台高性能方案(如 `MessagePack`、`Protobuf`)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
- **文件流序列化**:直接将对象保存到磁盘文件中。
|
||||
|
||||
- **内存流序列化**:对象 → 字节数组 → 可保存或传输。
|
||||
|
||||
- **反序列化**:根据文件流或字节数组还原对象。
|
||||
|
||||
|
||||
序列化与反序列化是 C# 中对象持久化与数据传输的核心知识点,实际开发中需结合安全性与场景选择合适的序列化方式。
|
||||
@@ -0,0 +1,109 @@
|
||||
#VPS
|
||||
#节点搭建
|
||||
通过racknerd购买
|
||||
订单号1495436953
|
||||
邮箱:q1581603785@gmail.com
|
||||
IP地址: 104.223.59.197
|
||||
登录名称: root
|
||||
密码: 8r1h7y3X9MXIhnq0BC
|
||||
SSH端口号:22/35876
|
||||
系统:Debian 12
|
||||
目前已经给VPS服务器增加了密钥登录
|
||||
C:\Users\Administrator\.ssh\id_ed25519 是我们的VPS的密钥
|
||||
|
||||
修改密钥登录/密码登录
|
||||
sudo nano /etc/ssh/sshd_config
|
||||
进入该config配置,找到PasswordAuthentication 修改为yes就是密码登录 no就是密钥登录。
|
||||
输入完毕后ctrl+O保存 Ctrl+X 关闭
|
||||
输入systemctl restart ssh 是配置生效
|
||||
|
||||
X-UI端口号:9859
|
||||
|
||||
XUI登录网址:https://104.223.59.197:9859/A58sNK2JcqArntRYdE/panel/
|
||||
|
||||
通过VPS服务商登入VPS后台:
|
||||
ID:vmuser315094
|
||||
密码:gn0sxJ5i9a
|
||||
VPS的服务商是racknerd
|
||||
|
||||
域名网站:dynadot
|
||||
安全码:1234
|
||||
安全问题:秦豪凯 汝州 番茄炒鸡蛋
|
||||
|
||||
|
||||
vps服务器通过域名搭建节点
|
||||
证书保存地址
|
||||
[INF] - Certificate File: /root/cert/vps.kingishu.lol/fullchain.pem
|
||||
[INF] - Private Key File: /root/cert/vps.kingishu.lol/privkey.pem
|
||||
域名登录UI面板
|
||||
Access URL: https://vps.kingishu.lol:9859/A58sNK2JcqArntRYdE/
|
||||
账号:Kingishu(区分大小写)
|
||||
密码:qhkan1314
|
||||
|
||||
|
||||
## 创建节点
|
||||
1.不通过域名创建节点(ip暴露)
|
||||
协议:vless
|
||||
传输:TCP
|
||||
安全:Reality
|
||||
设置Target和SNI,使用国际大厂即可
|
||||
我们设置为特斯拉
|
||||
Target:www.tesla.com:443
|
||||
SNI:www.tesla.com
|
||||
2.通过域名创建节点(通过域名不暴露ip)
|
||||
协议:vmess
|
||||
端口:2053(网上搜索常用端口)
|
||||
设置主机为:我们的域名
|
||||
路径:随意设置
|
||||
安全:TLS
|
||||
SNI:我们的域名
|
||||
点击从面板设置证书
|
||||
|
||||
//下一步 CDN节点优化
|
||||
//优选IP 优选IP订阅器
|
||||
优选IP的github仓库:[https://github.com/XIU2/CloudflareSpe...](https://www.youtube.com/redirect?event=video_description&redir_token=QUFFLUhqbmQ0LWo0NnBMaHRNal94YkFpNEpiQjNYb3Y5Z3xBQ3Jtc0tscHVLeFBjbl9lLUtyX0d0cTRycVFfbkFpUmpyYTZKclE2UzhxUzdrTkhqS3pValVSOU9hV2RwWTVVaVZoMWpPT3hWVVBMaTMwcS1GU1VtZzQzQmtRcG1GYmFaWk1Ra3JZRkEwXy1hY0ZhYU9zdi03RQ&q=https%3A%2F%2Fgithub.com%2FXIU2%2FCloudflareSpeedTest&v=E5PI0LsQ43M)
|
||||
打开使用即可
|
||||
订阅器的Github仓库[https://github.com/InfiCheesy/cloudfl...](https://www.youtube.com/redirect?event=video_description&redir_token=QUFFLUhqbDg3dm1jbWY1V0hXVG5fNEFxNnBUeXVDVVgtZ3xBQ3Jtc0trUVN3ekY2LXZKajN5aEZkbldKbnppNk1WVEM0a0ktR2l6WU92NVZmTUxiUlJ0SVVTWjNDd3h4RnE3UEhyQk9wYy1DMFNvb3R6ck5scExGNC1GLUpIbmQ1SVhXbGEwaGNOOHVUVnQ3YXQ5dnI4VjFWbw&q=https%3A%2F%2Fgithub.com%2FInfiCheesy%2Fcloudflaresub&v=E5PI0LsQ43M)
|
||||
订阅器需要部署
|
||||
打开CloudFlare,左边的Workers和Pages
|
||||
链接github仓库,部署上面的订阅器github
|
||||
部署完毕,左侧选择存储和数据库,选择WorkersKV,名字随便写,里面
|
||||
|
||||
|
||||
PrivateKey = CLqwbJti7j6o4OMmSq7ZKvOJucriWiRz8kqm3Z5bc1o=
|
||||
Address = 172.16.0.2/32
|
||||
PublicKey = bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=
|
||||
|
||||
|
||||
|
||||
//重装系统之后的新内容
|
||||
主机IP:104.223.59.156
|
||||
用户名:root
|
||||
VPS服务器登录端口修改:35876
|
||||
登录SSH密码:qhkan1314
|
||||
//UI面板信息
|
||||
Username: jOX3gF1ZnF
|
||||
Password: lqYYDRvMuJ
|
||||
Port: 7127
|
||||
WebBasePath: QvZUvPLajW7QC23N0C
|
||||
Access URL: https://104.223.59.197:7127/QvZUvPLajW7QC23N0C
|
||||
登录地址: https://vps.kingishu.lol:7127/QvZUvPLajW7QC23N0C/
|
||||
|
||||
|
||||
https://104.223.59.156:7127/QvZUvPLajW7QC23N0C
|
||||
|
||||
|
||||
|
||||
# 谷歌美区老号
|
||||
账号:BalrajPhinn@gmail.com
|
||||
旧密码:30l5wbetgjr(无效,仅记录旧密码)
|
||||
新密码:qhkan1314
|
||||
辅助邮箱:logepomaresji661@yahoo.com
|
||||
新辅助邮箱:653076247@qq.com
|
||||
2FA:hd3o zdvd 7qjx uu5e qhju nbae pffg fqdv
|
||||
|
||||
购买渠道:https://store.dimosky.com/
|
||||
订单查询邮箱:q1581603785@gmail.com
|
||||
查询密码: qhkan1314
|
||||
|
||||
2FA验证器:https://2fa.show/
|
||||
@@ -0,0 +1,11 @@
|
||||
## 常用命令
|
||||
1.复制,鼠标左键选中自动拖拽
|
||||
2.粘贴,鼠标中键粘贴
|
||||
3.使用Unix风格路径
|
||||
比如进入D:\item\ARPG
|
||||
我们使用 cd /d/item/ARPG
|
||||
4.链接SSH服务器
|
||||
ssh 用户名@IP地址 -p 端口号
|
||||
例如ssh root@104.223.59.156 -p 35876
|
||||
5.换行
|
||||
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 420 KiB |
@@ -0,0 +1,414 @@
|
||||
### **第一章:Git 是什么?—— 你的时光机和协作利器**
|
||||
|
||||
想象一下,你正在做一个大型的 Unity 项目,你和你的队友们分别负责不同的模块。
|
||||
|
||||
- **没有版本控制的噩梦:**
|
||||
|
||||
- 你写好了一个功能,用微信发给主程,他手动合并到主项目里。
|
||||
|
||||
- 美术同学做好了一个新模型,替换了旧的,结果发现新模型有问题,但旧文件已经被覆盖,找不回来了。
|
||||
|
||||
- 你和另一个程序员同时修改了 `PlayerController.cs` 脚本,最后提交的人把另一个人的代码给覆盖了,导致功能丢失。
|
||||
|
||||
- 一周后,游戏出了个大 Bug,你发现是你三天前某次修改引入的,但你已经不记得当时改了什么,也找不到三天前的版本了。
|
||||
|
||||
|
||||
**Git 就是为了解决以上所有问题而生的。**
|
||||
|
||||
它是一个**分布式版本控制系统 (Distributed Version Control System, DVCS)**。我们拆解一下:
|
||||
|
||||
1. **版本控制:** 它可以帮你记录项目文件的每一次变化(谁在什么时间,改了哪个文件的哪几行)。你可以随时“回到过去”,查看任何一个历史版本。
|
||||
|
||||
2. **分布式:** 这是 Git 与早期工具 (如 SVN) 最大的不同。在 SVN 中,所有人都连接一个中央服务器。而 Git 中,**每个开发者的电脑上都有一个完整的项目仓库(Repository)**,包含所有的历史记录。这意味着:
|
||||
|
||||
- 你可以在没有网络的情况下,在本地提交代码、查看历史、创建分支。
|
||||
|
||||
- 即使远程服务器(比如 GitHub)挂了,你的本地仓库也是安全的,不会丢失任何数据。
|
||||
|
||||
|
||||
#### **为什么 Unity 项目尤其需要 Git?**
|
||||
|
||||
Unity 项目不仅有代码 (`.cs` 文件),还有大量的**二进制文件**,比如场景 (`.unity`)、预制体 (`.prefab`)、模型 (`.fbx`)、贴图 (`.png`) 等。Git 结合一个叫做 **Git LFS** 的工具,可以完美地管理这些“大块头”,让团队协作变得井然有序。
|
||||
|
||||
---
|
||||
|
||||
### **第二章:核心概念 —— Git 的世界观**
|
||||
|
||||
在敲任何命令之前,你必须先理解 Git 是如何“思考”的。这几个概念是基石中的基石。
|
||||
|
||||
#### **2.1 仓库 (Repository / Repo)**
|
||||
|
||||
简单说,就是你的项目文件夹。这个文件夹里的一切(代码、资源、修改历史)都被 Git 管理着。
|
||||
|
||||
- **本地仓库 (Local Repository):** 存放在你自己电脑上的仓库。你日常的修改、提交等绝大部分操作都是在这里完成。
|
||||
|
||||
- **远程仓库 (Remote Repository):** 存放在一台服务器上(比如 GitHub、Gitee 或公司自己的 GitLab),供团队成员共享和同步代码。它是团队协作的“集线器”。
|
||||
|
||||
|
||||
#### **2.2 Git 的三大区域**
|
||||
|
||||
这是 Git 的精髓所在,理解了它,你就理解了一半的 Git 命令。
|
||||
|
||||
1. **工作区 (Working Directory):** 就是你在电脑上能直接看到的项目文件夹。你用 Unity Editor、VS Code 等工具做的所有修改,都发生在这里。
|
||||
|
||||
2. **暂存区 (Staging Area / Index):** 一个“待提交”的区域。当你对工作区的文件做了修改后,你可以通过 `git add` 命令,把这些修改“放”到暂存区。这给了你极大的灵活性,比如你改了10个文件,但这次只想提交其中的3个,你就可以只 `add` 这3个文件。
|
||||
|
||||
3. **本地仓库 (Local Repository):** 当你觉得暂存区里的内容可以作为一次完整的“版本”时,你使用 `git commit` 命令,Git 会将暂存区的所有内容生成一个“快照”(我们称之为一次 **提交 (Commit)**),并永久保存在你的本地仓库中。
|
||||
|
||||
|
||||
**它们之间的关系如下图:**
|
||||
|
||||
#### **2.3 提交 (Commit)**
|
||||
|
||||
一次 **Commit** 就是你对项目做的一次有意义的修改的“存档点”。它包含:
|
||||
|
||||
- 一个独一无二的 ID(一个长长的字符串,叫做 SHA-1 哈希值)。
|
||||
|
||||
- 提交者的信息(你的名字和邮箱)。
|
||||
|
||||
- 提交的时间。
|
||||
|
||||
- **提交信息 (Commit Message):** 这是最重要的部分!用来清晰地描述“你这次提交干了什么”。**写好 Commit Message 是团队协作的灵魂!**
|
||||
|
||||
|
||||
#### **2.4 分支 (Branch)**
|
||||
|
||||
如果说 Commit 是存档点,那 **分支** 就是平行的“故事线”。
|
||||
|
||||
- **主分支 (`master` 或 `main`):** 默认存在的分支。在团队中,它通常代表着最稳定、可随时发布的版本。
|
||||
|
||||
- **开发分支 (`develop`):** 通常我们的主要开发工作都在这个分支上进行。
|
||||
|
||||
- **功能分支 (`feature`):** 这是团队协作的核心。当你要开发一个新功能时(比如“新的背包系统”),你会从 `develop` 分支上创建一个新的分支,比如 `feature/inventory-system`。然后你在这个新分支上尽情地编写代码、测试,完全不会影响到 `develop` 分支的稳定性。当功能开发完毕后,再把它 **合并 (Merge)** 回 `develop` 分支。
|
||||
|
||||
|
||||
**这种“先开分支 -> 再开发 -> 最后合并”的模式,是保证团队项目稳定、高效协作的基石。**
|
||||
|
||||
---
|
||||
|
||||
### **第三章:准备工作 —— 为 Unity 项目量身定制 Git**
|
||||
|
||||
在开始使用 Git 之前,我们需要做一些针对 Unity 项目的特殊配置。
|
||||
|
||||
#### **3.1 安装与配置**
|
||||
|
||||
1. **安装 Git:** 直接从 [Git 官网](https://git-scm.com/downloads) 下载并安装。Windows 用户建议一路默认安装,这样会附带一个非常好用的命令行工具 **Git Bash**。
|
||||
|
||||
2. **初次配置:** 安装后,打开 Git Bash(或终端),设置你的身份。这个身份会记录在你的每一次提交里。
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
git config --global user.name "Your Name" # 替换成你的名字或常用昵称
|
||||
git config --global user.email "you@example.com" # 替换成你的邮箱
|
||||
```
|
||||
|
||||
|
||||
#### **3.2 `.gitignore`:告诉 Git 该忽略什么**
|
||||
|
||||
Unity 在运行时会生成大量临时文件和本地缓存(比如 `Library` 文件夹),这些文件体积巨大且对其他团队成员毫无用处。我们必须告诉 Git 忽略它们,否则你的仓库会变得臃肿不堪,并且充满冲突。
|
||||
|
||||
在你的 Unity 项目根目录(与 `Assets` 和 `ProjectSettings` 文件夹同级)创建一个名为 `.gitignore` 的文本文件,然后把 [GitHub 官方推荐的 Unity .gitignore 内容](https://github.com/github/gitignore/blob/main/Unity.gitignore) 复制进去。
|
||||
|
||||
**核心要点是,它会忽略掉 `Library`, `Temp`, `Logs`, `Build` 等文件夹。**
|
||||
|
||||
#### **3.3 `Git LFS`:管理大文件的神器**
|
||||
|
||||
Git 本身不擅长处理大的二进制文件(模型、贴图、音频等)。Git LFS (Large File Storage) 是一个专门解决此问题的扩展。它的原理是,在 Git 仓库里只保存大文件的“指针”(一个很小的文本文件),而真实的文件内容则存储在一个专门的 LFS 服务器上。
|
||||
|
||||
1. **安装 LFS:** 从 [Git LFS 官网](https://git-lfs.github.com/) 下载并安装。
|
||||
|
||||
2. **在项目中启用:** 在项目根目录打开 Git Bash,运行:
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
git lfs install
|
||||
```
|
||||
|
||||
3. **追踪文件类型:** 告诉 LFS 哪些类型的文件需要由它来管理。
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
git lfs track "*.psd" "*.png" "*.jpg" "*.fbx" "*.asset" "*.unity" "*.prefab"
|
||||
```
|
||||
|
||||
这个命令会创建一个 `.gitattributes` 文件。**`.gitignore` 和 `.gitattributes` 这两个文件都必须提交到 Git 仓库中!**
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **第四章:单人模式 —— 熟悉核心命令**
|
||||
|
||||
我们先在自己的电脑上,走一遍个人开发的全流程,熟悉最核心的命令。
|
||||
|
||||
#### **4.1 获取项目**
|
||||
|
||||
- **情况一:初始化一个全新的项目**
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
# 进入你的 Unity 项目文件夹
|
||||
cd path/to/your/unity/project
|
||||
|
||||
# 初始化 Git 仓库
|
||||
git init
|
||||
```
|
||||
|
||||
- **情况二:从远程仓库下载一个已有项目(团队中最常见的)**
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
# 克隆远程项目到本地
|
||||
git clone <远程仓库的URL> # 例如: https://github.com/your-team/your-project.git
|
||||
```
|
||||
|
||||
|
||||
#### **4.2 日常开发循环**
|
||||
|
||||
这是你每天都要重复无数次的“三板斧”。
|
||||
|
||||
1. **`git status` - 查看状态** 这是**最最常用**的命令!它会告诉你:
|
||||
|
||||
- 哪些文件被修改了?(modified)
|
||||
|
||||
- 哪些是新创建的文件?(untracked)
|
||||
|
||||
- 哪些文件已经被放入暂存区了?(staged) **养成随时随地敲 `git status` 的好习惯!**
|
||||
|
||||
2. **`git add <文件名>` - 添加到暂存区**
|
||||
|
||||
- 添加某个文件: `git add Assets/Scripts/Player.cs`
|
||||
|
||||
- 添加整个文件夹: `git add Assets/Scripts/`
|
||||
|
||||
- **添加所有修改和新文件(最常用):**
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
git add .
|
||||
```
|
||||
|
||||
3. **`git commit -m "提交信息"` - 提交到本地仓库**
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
git commit -m "feat: 实现玩家跳跃功能"
|
||||
```
|
||||
|
||||
`-m` 后面跟着的是本次提交的说明。**一定要写清楚!** 一个好的规范是:
|
||||
|
||||
- **feat:** 新功能
|
||||
|
||||
- **fix:** 修复 Bug
|
||||
|
||||
- **docs:** 修改文档
|
||||
|
||||
- **refactor:** 代码重构
|
||||
|
||||
- **style:** 代码格式调整
|
||||
|
||||
- **art:** 美术资源更新
|
||||
|
||||
|
||||
**示例:**
|
||||
|
||||
- `git commit -m "fix: 修复了打开商店UI时出现的空引用异常"`
|
||||
|
||||
- `git commit -m "art: 更新了主角色的行走动画"`
|
||||
|
||||
|
||||
#### **4.3 `git log` - 查看历史**
|
||||
|
||||
想看看你和队友们都干了什么?
|
||||
|
||||
- `git log`: 显示详细的提交历史。
|
||||
|
||||
- `git log --oneline`: 每个提交只显示一行,非常清晰。
|
||||
|
||||
- `git log --graph --oneline --decorate`: 以图形化的方式显示分支合并历史,强烈推荐!
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **第五章:团队协作模式 —— 分支与远程操作**
|
||||
|
||||
这才是 Git 真正强大的地方!
|
||||
|
||||
#### **5.1 远程仓库交互**
|
||||
|
||||
- **`git push` - 推送** 将你本地的提交推送到远程仓库,这样队友才能看到你的代码。
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
# 将本地的 develop 分支推送到名为 origin 的远程仓库
|
||||
git push origin develop
|
||||
```
|
||||
|
||||
- **`git pull` - 拉取** 从远程仓库获取最新的代码,并与你的本地分支合并。**这是你每天早上开始工作前,必须执行的第一个命令!**
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
# 拉取远程的 develop 分支并与本地合并
|
||||
git pull origin develop
|
||||
```
|
||||
|
||||
|
||||
#### **5.2 黄金流程:功能分支工作流 (Feature Branch Workflow)**
|
||||
|
||||
这是目前最流行、最安全的团队协作流程。
|
||||
|
||||
**场景:你要开发一个“玩家登录”功能。**
|
||||
|
||||
1. **第一步:更新主开发分支** 确保你的 `develop` 分支是最新版本。
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
git checkout develop # 1. 切换到 develop 分支
|
||||
git pull origin develop # 2. 从远程拉取最新代码
|
||||
```
|
||||
|
||||
2. **第二步:创建你的功能分支** 分支命名要清晰,比如 `feature/user-login` 或 `fix/bug-123`。
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
# 创建并切换到新分支
|
||||
git checkout -b feature/user-login
|
||||
```
|
||||
|
||||
3. **第三步:在新分支上尽情开发** 现在,你可以安心地在新分支上进行开发了。不断地 `add` 和 `commit`。
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
# ... 编写代码,修改资源 ...
|
||||
git add .
|
||||
git commit -m "feat: 完成登录UI界面搭建"
|
||||
# ... 继续工作 ...
|
||||
git add .
|
||||
git commit -m "feat: 对接登录服务器API"
|
||||
```
|
||||
|
||||
你在 `feature/user-login` 分支上的所有操作,都与 `develop` 分支无关,可以大胆尝试。
|
||||
|
||||
4. **第四步:将你的分支推送到远程** 这既是备份,也是为了让其他同事可以看到你的进度。
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
git push origin feature/user-login
|
||||
```
|
||||
|
||||
5. **第五步:发起合并请求 (Pull Request / PR)** 当你的功能开发完成并通过自测后,最关键的一步来了。你需要通过远程仓库的网页界面(GitHub/Gitee 等)发起一个 **Pull Request**。 它的意思是:“我 (`feature/user-login`) 的功能做完了,请求项目负责人审查我的代码,并把它合并到 `develop` 分支。” 这是进行 **代码审查 (Code Review)** 的最佳时机。你的同事或主管会检查你的代码,提出修改意见。
|
||||
|
||||
6. **第六步:合并与清理** 一旦 PR 被批准,负责人会在网页上点击“Merge”按钮,你的所有代码就正式汇入 `develop` 分支了! 之后,这个功能分支就完成了它的历史使命,可以被删除了。
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
# 切换回 develop 分支
|
||||
git checkout develop
|
||||
|
||||
# 删除本地分支
|
||||
git branch -d feature/user-login
|
||||
|
||||
# 删除远程分支
|
||||
git push origin --delete feature/user-login
|
||||
```
|
||||
|
||||
|
||||
#### **5.3 解决冲突 (Merge Conflict) —— 团队协作的必修课**
|
||||
|
||||
当 A 和 B 同时修改了同一个文件的同一块区域时,`git pull` 或 `git merge` 时就会产生冲突。Git 不知道该听谁的,于是把决定权交给你。
|
||||
|
||||
1. **识别冲突:** `git pull` 后,Git 会在终端提示哪些文件冲突了。
|
||||
|
||||
2. **解决冲突:** 打开冲突的文件,你会看到类似这样的标记:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
<<<<<<< HEAD
|
||||
// 这是你本地的代码
|
||||
public float speed = 10.0f;
|
||||
=======
|
||||
// 这是远程仓库上你队友的代码
|
||||
public float speed = 15.0f;
|
||||
>>>>>>> abcdef123...
|
||||
```
|
||||
|
||||
3. **做出决定:** 你需要和队友沟通,或者根据需求,手动修改这部分代码,决定最终的版本。比如,你们决定速度应该是 `12.0f`。
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
// 删除所有特殊标记,留下最终的代码
|
||||
public float speed = 12.0f;
|
||||
```
|
||||
|
||||
4. **完成合并:**
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
git add <刚刚解决冲突的文件名>
|
||||
git commit # 此时不需要 -m,Git 会自动生成一个合并的提交信息
|
||||
```
|
||||
|
||||
|
||||
**Unity 场景和预制体冲突是天坑!** `.unity` 和 `.prefab` 是二进制文件,它们的冲突极难用文本方式解决。 **避免策略:**
|
||||
|
||||
- **责任到人:** 尽可能保证同一时间只有一个人在修改同一个场景或 Prefab。
|
||||
|
||||
- **化整为零:** 尽量将大场景拆分成多个小 Prefab,分给不同的人负责。
|
||||
|
||||
- **加强沟通:** 在修改一个公共 Prefab 或核心场景前,在团队群里吼一声:“我要改主场景的摄像机了,其他人先别动!”
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **第六章:高级技巧与最佳实践**
|
||||
|
||||
- **`git stash` - 临时储藏:** 当你正在开发一个功能,突然来了个紧急 Bug 要修,但你手头的代码还没写完,不想提交。怎么办?
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
git stash # 将当前所有未提交的修改临时存起来
|
||||
# ... 切换到别的分支修复 Bug,提交 ...
|
||||
git checkout feature/your-original-branch # 切回你原来的分支
|
||||
git stash pop # 把之前存起来的修改恢复回来
|
||||
```
|
||||
|
||||
- **图形化工具 (GUI):** 虽然命令行是基础,但在查看复杂的提交历史、处理分支和解决冲突时,使用图形化工具会更直观。推荐:**SourceTree**, **Fork**, 或者 **VS Code 自带的 Git Lens 插件**。它们可以帮你清晰地看到分支图和文件差异。
|
||||
|
||||
- **提交前编译:** 在 `git commit` 之前,**一定**要确保你的项目在 Unity Editor 里能正常编译通过。提交一个带编译错误的版本给团队,是非常不负责任的行为。
|
||||
|
||||
- **保持仓库整洁:** 定期清理已经合并的、无用的本地和远程分支。
|
||||
|
||||
|
||||
### **总结**
|
||||
|
||||
掌握 Git 不是一朝一夕的事,但它绝对是你职业生涯中最值得投资的技能之一。
|
||||
|
||||
**给你的学习路径建议:**
|
||||
|
||||
1. **理解核心概念:** 仓库、三个区域、提交、分支。这是地基。
|
||||
|
||||
2. **熟练个人流程:** `status`, `add`, `commit`, `log`。这是日常。
|
||||
|
||||
3. **上手团队流程:** `pull`, `push`, `branch`, `merge`。这是协作。
|
||||
|
||||
4. **在实践中学习:** 不要怕犯错,主动去创建分支,甚至故意制造一些冲突来练习解决。Git 强大的地方在于,它几乎总能让你安全地回到任何一个历史版本。
|
||||
|
||||
|
||||
从今天起,忘掉拖动文件夹和 U 盘吧。拥抱 Git,它会让你的开发工作变得前所未有的清晰、高效和安全。祝你在 Unity 的世界里,借助 Git 的翅膀,飞得更高!
|
||||
@@ -0,0 +1,31 @@
|
||||
#### 1.委托和事件的区别
|
||||
#### 2.使用数组实现栈,队列,List
|
||||
#### 3.手撕排序算法
|
||||
#### 4.\==运算符和equals方法的区别
|
||||
#### 5.接口和抽象类的区别(从继承规则、成员类型、使用场景角度分析)
|
||||
区别:①继承:抽象类单继承,接口多实现;②成员:抽象类可包含非抽象成员,接口(C#8.0 前)全抽象;③场景:抽象类体现 “is-a” 关系(如 “狗是动物”),接口体现 “can-do” 能力(如 “会飞”)。
|
||||
#### 6.值类型和引用类型的区别
|
||||
#### 7.如何在不使用第三个变量的情况下交换变量
|
||||
|
||||
##### 8.C#中的String和string的区别是什么
|
||||
|
||||
##### 9.readonly和const的区别是什么
|
||||
|
||||
10.讲一下c#的拆箱和装箱
|
||||
|
||||
11.静态变量和非静态变量有什么区别
|
||||
|
||||
12.方法重载和方法override的区别是什么
|
||||
|
||||
13.c#的泛型是怎么一回事,它的作用是什么
|
||||
|
||||
14.c#的string类为什么是不可变的
|
||||
|
||||
15.String类和string类的区别是什么
|
||||
|
||||
16.string和StringBuilder的区别是什么
|
||||
|
||||
17.c#的lambda表达式是什么,带来了什么便利
|
||||
|
||||
18.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#BUG记录
|
||||
### 1.洗炼界面选中图片的位置BUG
|
||||
**背景**:在ARPG游戏开发中,默认打开,我们需要现实第一个装备的信息,默认选中第一个装备,这样就需要将Image设置到我们第一个装备的位置,我们在onenable中完成了代码逻辑,运行发现错误,Image的位置并不符合。
|
||||
|
||||
**问题原因**:UGUI布局刷新时机问题,我们在OnEnable中调用ShowEquip来显示装备,在这一帧UGUI还没有对新的背包进行重新计算坐标,这时候我们通过Find找到对应格子的位置,获取到的是错误的,因此赋值给Image也错误了
|
||||
|
||||
**解决办法**:在获取格子位置之前,通过Canvas.ForceUpdateCanvases();强制刷新UI布局,让UGUI计算坐标
|
||||
@@ -0,0 +1,8 @@
|
||||
上午
|
||||
- 问AI关于FMOD相关的内容,研究怎么自定义播放音频
|
||||
下午
|
||||
- 实现了表情播放完毕之后根据表情名字的不同,播放不同的音效。
|
||||
- 音效通过JSON数据解析。
|
||||
- 看了小框架-事件中心模块的内容。
|
||||
晚上
|
||||
- 给小框架-事件中心模块看完了,自己实现了一遍
|
||||
@@ -0,0 +1,24 @@
|
||||
## 3.18学习题目
|
||||
1.二分查找
|
||||
2.两数之和
|
||||
3.三数之和 ❌
|
||||
4.反转链表
|
||||
5.环形链表
|
||||
6.合并两个有序链表
|
||||
7.分割链表
|
||||
8.删除链表的倒数第n个节点
|
||||
9.链表的中间节点
|
||||
10.环形链表2
|
||||
11.相交链表(用set可以做出来,用连接的方法没做出来)
|
||||
.学习广度优先遍历和深度优先遍历的思想
|
||||
使用c++手撕,做到5分钟一道题,不用思考
|
||||
## 3.19学习题目
|
||||
1.回文链表
|
||||
2.二叉树的最大深度
|
||||
3.二叉树中的所有路径
|
||||
4.递归法实现反转链表
|
||||
5.所有排序
|
||||
6.c++常用容器,以及他们的方法
|
||||
7.topK问题
|
||||
8.最大连续子数组的和
|
||||
9.求集合的所有自己
|
||||
@@ -0,0 +1,4 @@
|
||||
1.冒泡排序
|
||||
时间复杂度O(n²),稳定排序,自适应排序,原地排序
|
||||
适用情况:数据近乎于有序
|
||||
2.
|
||||
@@ -0,0 +1,7 @@
|
||||
记录学习过程中,遇到的,学过但是遗忘的知识
|
||||
### 3.25 编程入门20题
|
||||
1.最小公倍数
|
||||
2.按位异或操作符
|
||||
3.二维数组和交错数组
|
||||
4.绝对值
|
||||
5.ASCII码(字符串和字符数组转换)
|
||||
@@ -0,0 +1,20 @@
|
||||
1.散列表,线性探测,冲突处理
|
||||
2.二分查找次数问题
|
||||
3.二叉树前中后序遍历问题
|
||||
4.堆排序
|
||||
5.希尔排序
|
||||
6.数组插入问题
|
||||
7.队列入队出队顺序问题
|
||||
8.栈的入栈出栈顺序问题
|
||||
9.三个节点的二叉树可能出现多少种结构?
|
||||
10.连通图的邻接矩阵问题
|
||||
11.哈夫曼编码问题(用二进制来编码字符串 ABCDABAA 需要能根据编码解码回原来的字符串。最少需要多长的二进制字符串呢?)
|
||||
12.快速排序问题
|
||||
13.内存溢出,内存泄露,指针丢失
|
||||
14.并发操作带来的数据不一致
|
||||
15.某系统有 4 个并发进程,需要同类资源 5 个,当系统中这类资源最少数是几个的时候,系统一定不会发生死锁。
|
||||
16.OSI模型
|
||||
17.反转链表
|
||||
18.哈希表,二叉树,链表
|
||||
19.斐波那契数列问题
|
||||
20.mysql数据库问题
|
||||
@@ -0,0 +1,26 @@
|
||||
1.LeetCode35 搜索插入位置
|
||||
2.LeetCode1302 层数最深叶子节点的和
|
||||
3.链表的环,以及入口
|
||||
4.Top-K算法
|
||||
5.全排列
|
||||
6.给出n对括号,请编写一个函数来生成所有的由n对括号组成的合法组合。例如,给出n=3,解集为:
|
||||
"((O))”, “(00)", “(0)0", “000","()(0)"
|
||||
7.数组左移k位
|
||||
8.集合的全部子集
|
||||
9.![[Pasted image 20260320020450.png]]
|
||||
10.场景题:实现一个玩家和小怪的demo,玩家和小怪分别有生命值、攻击力、护甲等属性,回合制,可以选择进攻和防御,玩家先手
|
||||
11.邮件类和邮箱类(要维护已读和未读),能把邮件发给指定邮箱
|
||||
12.算法题
|
||||
- 实现一个道具,有类型、品质、最大道具数量等属性
|
||||
- 实现一个道具管理类,包含具体的增删查改接口
|
||||
- 根据类型、品质、最大道具数量进行排序,输出一个列表
|
||||
- 假设一个背包有10000个道具,如何优化使其流畅
|
||||
13.定义三种角色,有名字、性别、年龄等属性,每种角色可以动态地设置属性,然后还要设置对某些事物的访问权限。
|
||||
14.用数组实现队列。包含入队和出队的方法。
|
||||
15.数组扁平化
|
||||
16.给定一组非负整数nums,重新排列每个数的顺序,使之组成一个最大的整数
|
||||
输入:nums = [3,30,34,5,9] 输出"9534330"
|
||||
17.
|
||||
给一个整数,转换为对应的字符串,可以是负数,单位:个、十、百、千、......、亿、兆、百兆
|
||||
比如整数“10020”转化为字符串“一万零二十”
|
||||
18.
|
||||
@@ -0,0 +1,22 @@
|
||||
1.表命名方式 xxx_xxx.xlsx 如:兵器配置_Weapon.xlsx,中文和英文字段组成,英文字段会转化为程序使用的脚本名称.所以不要出现重复的英文名称.
|
||||
2.数据配置在##页签,一个表可以包含多个页签,但是只会读取##页签中的数据
|
||||
3.##页签的第一到第二行为预留行,可以写备注
|
||||
4.##页签的第三到第六行,依次为:适用范围(0通用 1前端 2后端)、字段描述、字段英文名称(程序用)、数据类型
|
||||
|
||||
配置表支持的类型:
|
||||
int 整数类型 范围是 -2,147,483,648 到 +2,147,483,647
|
||||
long 长整数类型 一般用于表示时间戳 范围是-9,223,372,036,854,775,808 到 +9,223,372,036,854,775,807
|
||||
bool 表示是与否 TRUE则为是 FALSE则为否
|
||||
string 通常用于表示文本描述
|
||||
--Vector3 通常用于表示位置xyz(已取消)
|
||||
float 通常用于表示带有小数点的数据,如1.57
|
||||
[暂定不用,解析效率太慢]json 用于特殊配置,可以同时包含多个不同的key,以及对应的数据。尽量少用,读取速度没直接配置字段快,且性能会有一定的影响。
|
||||
|
||||
数组:
|
||||
支持int[] long[] bool[] string[] float[] 每个元素用英文逗号隔开,如:1,2,3,4,5....如果是字符串数组,字符串包含逗号则请用中文逗号,不要使用英文逗号,否则会导致最后生成的配置出错.
|
||||
|
||||
|
||||
重要:
|
||||
同一个表内,第一列为唯一索引,不要出现重复的ID
|
||||
如果某一列为 策划注释 非程序需要的字段,请在该列的第六行添加"注释"两字(不带双引号).
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
1.表命名方式 xxx_xxx.xlsx 如:兵器配置_Weapon.xlsx,中文和英文字段组成,英文字段会转化为程序使用的脚本名称.所以不要出现重复的英文名称.
|
||||
2.数据配置在##页签,一个表可以包含多个页签,但是只会读取##页签中的数据
|
||||
3.##页签的第一到第二行为预留行,可以写备注
|
||||
4.##页签的第三到第六行,依次为:适用范围(0通用 1前端 2后端)、字段描述、字段英文名称(程序用)、数据类型
|
||||
|
||||
配置表支持的类型:
|
||||
int 整数类型 范围是 -2,147,483,648 到 +2,147,483,647
|
||||
long 长整数类型 一般用于表示时间戳 范围是-9,223,372,036,854,775,808 到 +9,223,372,036,854,775,807
|
||||
bool 表示是与否 TRUE则为是 FALSE则为否
|
||||
string 通常用于表示文本描述
|
||||
--Vector3 通常用于表示位置xyz(已取消)
|
||||
float 通常用于表示带有小数点的数据,如1.57
|
||||
[暂定不用,解析效率太慢]json 用于特殊配置,可以同时包含多个不同的key,以及对应的数据。尽量少用,读取速度没直接配置字段快,且性能会有一定的影响。
|
||||
|
||||
数组:
|
||||
支持int[] long[] bool[] string[] float[] 每个元素用英文逗号隔开,如:1,2,3,4,5....如果是字符串数组,字符串包含逗号则请用中文逗号,不要使用英文逗号,否则会导致最后生成的配置出错.
|
||||
|
||||
|
||||
重要:
|
||||
同一个表内,第一列为唯一索引,不要出现重复的ID
|
||||
如果某一列为 策划注释 非程序需要的字段,请在该列的第六行添加"注释"两字(不带双引号).
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
1.表命名方式 xxx_xxx.xlsx 如:兵器配置_Weapon.xlsx,中文和英文字段组成,英文字段会转化为程序使用的脚本名称.所以不要出现重复的英文名称.
|
||||
2.数据配置在##页签,一个表可以包含多个页签,但是只会读取##页签中的数据
|
||||
3.##页签的第一到第二行为预留行,可以写备注
|
||||
4.##页签的第三到第六行,依次为:适用范围(0通用 1前端 2后端)、字段描述、字段英文名称(程序用)、数据类型
|
||||
|
||||
配置表支持的类型:
|
||||
int 整数类型 范围是 -2,147,483,648 到 +2,147,483,647
|
||||
long 长整数类型 一般用于表示时间戳 范围是-9,223,372,036,854,775,808 到 +9,223,372,036,854,775,807
|
||||
bool 表示是与否 TRUE则为是 FALSE则为否
|
||||
string 通常用于表示文本描述
|
||||
--Vector3 通常用于表示位置xyz(已取消)
|
||||
float 通常用于表示带有小数点的数据,如1.57
|
||||
[暂定不用,解析效率太慢]json 用于特殊配置,可以同时包含多个不同的key,以及对应的数据。尽量少用,读取速度没直接配置字段快,且性能会有一定的影响。
|
||||
|
||||
数组:
|
||||
支持int[] long[] bool[] string[] float[] 每个元素用英文逗号隔开,如:1,2,3,4,5....如果是字符串数组,字符串包含逗号则请用中文逗号,不要使用英文逗号,否则会导致最后生成的配置出错.
|
||||
|
||||
|
||||
重要:
|
||||
同一个表内,第一列为唯一索引,不要出现重复的ID
|
||||
如果某一列为 策划注释 非程序需要的字段,请在该列的第六行添加"注释"两字(不带双引号).
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
1.表命名方式 xxx_xxx.xlsx 如:兵器配置_Weapon.xlsx,中文和英文字段组成,英文字段会转化为程序使用的脚本名称.所以不要出现重复的英文名称.
|
||||
2.数据配置在##页签,一个表可以包含多个页签,但是只会读取##页签中的数据
|
||||
3.##页签的第一到第二行为预留行,可以写备注
|
||||
4.##页签的第三到第六行,依次为:适用范围(0通用 1前端 2后端)、字段描述、字段英文名称(程序用)、数据类型
|
||||
|
||||
配置表支持的类型:
|
||||
int 整数类型 范围是 -2,147,483,648 到 +2,147,483,647
|
||||
long 长整数类型 一般用于表示时间戳 范围是-9,223,372,036,854,775,808 到 +9,223,372,036,854,775,807
|
||||
bool 表示是与否 TRUE则为是 FALSE则为否
|
||||
string 通常用于表示文本描述
|
||||
--Vector3 通常用于表示位置xyz(已取消)
|
||||
float 通常用于表示带有小数点的数据,如1.57
|
||||
[暂定不用,解析效率太慢]json 用于特殊配置,可以同时包含多个不同的key,以及对应的数据。尽量少用,读取速度没直接配置字段快,且性能会有一定的影响。
|
||||
|
||||
数组:
|
||||
支持int[] long[] bool[] string[] float[] 每个元素用英文逗号隔开,如:1,2,3,4,5....如果是字符串数组,字符串包含逗号则请用中文逗号,不要使用英文逗号,否则会导致最后生成的配置出错.
|
||||
|
||||
|
||||
重要:
|
||||
同一个表内,第一列为唯一索引,不要出现重复的ID
|
||||
如果某一列为 策划注释 非程序需要的字段,请在该列的第六行添加"注释"两字(不带双引号).
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
1.Unity的Animator播放一个非循环动画,并且该动画没有链接任何其他动画,那么该动画播放完毕后,animator会卡在该动画结尾。但是normalizedTime会持续增加。
|
||||
2.
|
||||
@@ -0,0 +1,44 @@
|
||||
##### 1.加载界面卡顿
|
||||
我们在点击开始游戏的时候加载到游戏界面的过程中会卡顿,他会先卡顿一秒然后才进入到加载页面,他的问题就是SceneManager.LoadSceneAsync().
|
||||
Unity 的所有核心逻辑,包括UI渲染、输入处理和大部分游戏逻辑的执行,都发生在**主线程**上。如果主线程被一个耗时很长的任务(比如一次性从硬盘读取大量数据并解压)卡住,那么整个应用程序就会冻结——画面停止渲染,UI无法响应。这就是你遇到的“卡顿”。
|
||||
|
||||
- **`SceneManager.LoadSceneAsync()` 的“陷阱”**
|
||||
|
||||
- **用途**:它的设计初衷是为了在后台线程加载场景资源,避免长时间冻结主线程。
|
||||
|
||||
- **原理**:虽然名字里有 `Async`(异步),但它在被调用的**第一帧**,仍然需要在**主线程**上执行一些同步的准备工作。这包括:从磁盘定位场景文件、读取场景的元数据、准备反序列化等等。如果你的场景很大,有很多`GameObject`和资源引用,这个“准备工作”本身就可能耗时几十到几百毫秒,足以造成肉眼可见的卡顿。
|
||||
|
||||
- **好/不好**:这个函数本身是好的,是异步加载的唯一标准方式。但**不好**的地方在于,开发者很容易误解它,以为调用它的瞬间就是完全异步的。
|
||||
**代码1的问题剖析:**
|
||||
|
||||
```
|
||||
// 在主线程的某个 Update 或按钮点击事件中被调用
|
||||
public void Load(string next)
|
||||
{
|
||||
LoadingViewController.Instance.Open(); // 1. 请求打开UI
|
||||
LoginViewController.Instance.Close();
|
||||
|
||||
StartCoroutine(LoadSceneAsync(next)); // 2. 启动协程
|
||||
}
|
||||
|
||||
private IEnumerator LoadSceneAsync(string next)
|
||||
{
|
||||
// 3. 协程在下一帧开始执行
|
||||
var operation = SceneManager.LoadSceneAsync(next); // 4. 问题点!
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**执行流程与卡顿原因:**
|
||||
|
||||
1. **点击按钮(第N帧)**: `Load()` 方法被调用。它请求打开 Loading UI。但这只是一个请求,真正的UI绘制要等到这一帧的渲染阶段。
|
||||
|
||||
2. **启动协程**:`StartCoroutine` 启动了协程,但协程的代码要到**下一帧(第N+1帧)**的 `Update` 阶段才会开始执行。
|
||||
|
||||
3. **协程开始(第N+1帧)**: 协程的第一行代码 `SceneManager.LoadSceneAsync(next)` 被执行。
|
||||
|
||||
4. **主线程阻塞**:此时,`LoadSceneAsync` 的同步准备阶段开始,它会阻塞主线程。如果场景很大,主线程可能会在这里卡住 1-2 秒。
|
||||
|
||||
5. **卡顿后果**:因为主线程被阻塞,第N帧请求绘制的 Loading UI 根本没有机会被渲染出来。所以你的游戏画面会停留在上一个界面(LoginView),直到 `LoadSceneAsync` 的同步阶段完成。
|
||||
|
||||
6. **加载飞快**:当卡顿结束后,`LoadSceneAsync` 已经把大部分重活都干完了,`operation.progress` 的值可能直接就是 `0.8` 或接近 `0.9`。所以你的进度条看起来一瞬间就加载完了。
|
||||
@@ -0,0 +1,33 @@
|
||||
### 本文旨在记录射击游戏开发过程中遇到的问题,并提供解决方案
|
||||
##### 面朝一个方向的方法
|
||||
1.transform.forward
|
||||
2.transform.LookAt
|
||||
3.Quaternion.LookRotation
|
||||
第一个调整方法transform.forward=dir,修改我们的面朝方向的向量为dir,需要注意的是这里通常传入的是一个向量
|
||||
第二个方法transform.LookAt通常用来看向一个点,这就和上面的transform.forward=dir有所不同,第一种方法是向量,而Lookat通常望向一个点.
|
||||
第三种方法Quaternion.LookRotation,计算出来一个旋转,将这个旋转赋值给transform.forward就可以实现面向,LookAt内部就是调用了本方法.
|
||||
##### Transform方法的坐标系紊乱
|
||||
transform.Translate方法用来控制物体的移动,该方法不会考虑物理,只负责移动,注意该方法默认是在本地坐标系下运行的,如果需要在世界坐标系下完成操作,则需要在参数中添加.
|
||||
|
||||
##### 物体移动的方法
|
||||
- transform.Translate()
|
||||
- 设置transform.position+=
|
||||
- rb.AddForce()
|
||||
- 设置rb.velocity
|
||||
- rb.MovePosition()
|
||||
- CharacterController.Move()
|
||||
在不考虑物理的情况下可以使用方法一方法二
|
||||
在考虑物理的情况下可以根据情况选择方法三四五
|
||||
3D角色可以使用CharacterController
|
||||
|
||||
##### 物理系统
|
||||
Rigidbody控制物体的物理行为[[RigidBody]]
|
||||
Collider控制物体的形状[[碰撞器]]
|
||||
Material控制物理材质
|
||||
如果只有Rigidbody则该物体受到重力等各种物理效果,但是没有物体形状,不会发生碰撞,不会被射线检测到
|
||||
如果只有Collider,则可以被碰撞可以作为墙壁,但是没有物理效果
|
||||
##### 射线系统
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#### 项目设置
|
||||
- 版本使用2023.3及以上版本
|
||||
- 在preference中的scene view中勾选 creat objects at origin选项
|
||||
- 在package Manager中调整相关内容
|
||||
#### 素材导入
|
||||
- Icon图标
|
||||
- DOTween
|
||||
- Colourful Hierarchy Category GameObject
|
||||
- 课程自带素材
|
||||
@@ -0,0 +1,66 @@
|
||||
本节的难度比较大,基本上全部都是代码相关的问题,应该着重弄懂,可以默写.
|
||||
在上节课当中我们已经实现了对象池,可以从对象池中拿到卡牌,释放卡牌.这节课我们需要实现的内容是制作卡牌库实现抽卡.
|
||||
这里我们首先要弄明白,卡牌库,抽牌堆,手牌,弃牌堆这些内容.
|
||||
我们在游戏不断进行过程中,每一个关卡胜利之后,我们都会获得一张卡牌,去填充我们的卡牌库.
|
||||
在每一个关卡开始的时候,我们的抽牌堆会进行更新,将我们的卡牌库的所有牌都充入其中,同时抽出几张牌放入我们的手牌当中,当手牌打出就进入弃牌堆,当游戏结束剩余的卡牌也要进入弃牌堆,因此上述逻辑是我们本节课的关节内容.
|
||||
我们依照上述内容进行本节课的展开
|
||||
### 制作卡牌库
|
||||
我们的游戏是在不断的打怪升级中发展的,因此我们的卡牌库也是不断更新的,但是我们游戏最开始的时候有一套最初始的卡牌库,因此游戏当中的卡牌库一共有两个.
|
||||
1.游戏最开始的卡牌库
|
||||
2.随着游戏不断进行不断更新的卡牌库(当前卡牌库)
|
||||
确定好了卡牌库,我们要确定卡牌库的内容,我们的卡牌库要储存各种各样的卡牌,还要储存他们的数量,简化就是卡牌的种类和卡牌的数量,因此我们使用SO文件创建卡牌库,他当中有一个列表,列表的类型是自定义的类型,这个类型只有两个字段,一个CardDataSO表明类型,一个int表明数量.
|
||||
同时在我们的CardManager当中,声明这两个卡牌库,并且在初始化的时候,将游戏最开始的卡牌库赋值给当前卡牌库.
|
||||
|
||||
### 实现抽卡
|
||||
抽卡我们需要单独创建一个空物体,并且为他创造一个脚本来实现抽卡的相关逻辑.
|
||||
我们的抽卡,就是从抽牌堆中拿出一张牌,放入我们的手牌堆当中.
|
||||
因此我们需要这三个列表.
|
||||
```
|
||||
private List<CardDataSO> drawDeck = new(); //抽牌堆
|
||||
|
||||
private List<CardDataSO> disCardDeck = new();//弃牌堆
|
||||
|
||||
private List<Card> handCardObjectList = new();//当前的手牌(每回合)
|
||||
```
|
||||
|
||||
同时在游戏开始的时候我们要初始化我们的抽牌堆,这样就需要我们有CardManager,来初始化我们的抽牌堆,我们将CardManager也创建.
|
||||
在这节课当中,我们这个脚本需要实现的逻辑就是
|
||||
一.初始化抽牌库
|
||||
二.抽卡
|
||||
|
||||
1.初始化抽牌库我们需要创建一个函数,在start中调用,这个函数的内容就是使用循环语句,将
|
||||
CardManager当中的currentLibrary一一赋值给我们的抽牌堆,代码如下
|
||||
|
||||
```
|
||||
public void InitializeDeck()
|
||||
{
|
||||
drawDeck.Clear();//对抽牌堆进行清理
|
||||
//循环遍历当前的卡牌库,放进我们的抽牌堆
|
||||
foreach (var enter in cardManager.currentLibrary.cardLibraryList)
|
||||
{
|
||||
for (int i = 0; i < enter.amount; i++)
|
||||
{
|
||||
drawDeck.Add(enter.cardDataSO);
|
||||
}
|
||||
}
|
||||
//TODO: 洗牌/更新抽牌堆和弃牌堆
|
||||
}
|
||||
```
|
||||
2.抽卡就是需要我们从抽牌堆当中得到一个SO的文件,然后从对象池中生成一个卡片,并且将这个SO文件用来初始化这个卡牌,代码如下
|
||||
```
|
||||
public void DrawCard(int amount)
|
||||
{
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
if (drawDeck.Count == 0)
|
||||
{
|
||||
//TODO: 洗牌/更新抽牌堆和弃牌堆
|
||||
}
|
||||
CardDataSO currentCardData = drawDeck[0];
|
||||
drawDeck.RemoveAt(0);
|
||||
var card = cardManager.GetCard().GetComponent<Card>();//为什么这里会实例化出来一个新的card对象呢?
|
||||
card.Init(currentCardData);
|
||||
handCardObjectList.Add(card);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
这一节课同样的,没有新知识,全部都是代码思路.
|
||||
@@ -0,0 +1,13 @@
|
||||
### 1.在屏幕坐标转换为世界坐标的过程中,Z轴不同应该如何处理
|
||||
在2D游戏中,我们的摄像机Z坐标和场景的坐标一般是不同的,比如摄像机Z坐标为-10,场景坐标为0,那么如过我们在坐标转换的过程中,不特意的设置Z轴将产生一些问题.
|
||||
比如
|
||||
```
|
||||
Vector3 screenPos = new(Input.mousePosition.x, Input.mousePosition.y, 0);
|
||||
|
||||
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
|
||||
```
|
||||
上述代码可以在单击左键的时候进行坐标转换,但是我们传入的z轴为0,此时我们转换为世界坐标,他的z轴为-10.
|
||||
因为我们的摄像机坐标为-10,传入0相当于使用摄像机的坐标.如过我们想让我们的坐标在场景当中,也就是0应该怎么办呢?
|
||||
我们将z传入为10,也就是将屏幕坐标往z轴正方向移动10个单位,这时候获得的世界坐标z就是0.
|
||||
### 2.Unity当中的Physics 2D raycaster组件作用和原理
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
### 疑问点
|
||||
- 为什么RoomPrefab需要创建一个子物体sprite,在sprite上创建sprite Renderer,而不是直接在roomprefab上创建sprite Renderer?
|
||||
- 为什么调整图片大小,不调整父物体的scale,而是调整sprite的scale
|
||||
- 为什么碰撞体要添加在父物体身上?
|
||||
- 获取组件在哪个生命周期函数中获得?这些生命周期函数有什么不同?
|
||||
- 为什么在assets中创建了一个脚本Enums,里面专门写各种各样的枚举类型,别的脚本可以直接使用这些枚举类型
|
||||
- 我们的枚举类型声明时候没有特定指出生命在了哪个命名空间当中
|
||||
- 这是我们第一次使用SO文件作为参数配置,他的头文件每个参数代表的含义是什么?
|
||||
### 过程
|
||||
本质上是创建房间的prefab,以便于随机地图生成时直接实例化预制体,所以我们要创建一个房间的prefab,它上面需要挂载一个Room的脚本,里面包含了他自己的坐标位置X,Y,他的房间类型,他的房间状态。
|
||||
我们的房间状态,房间类型都是枚举类型,所以我们要写两个枚举类型。
|
||||
房间的类型,他需要有他自己的icon,有自己的类型,以及自己需要加载的场景,这是每一个房间都会有的,所以我们写为SO文件,以便于性能上的优化。
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
### 疑问
|
||||
- 为什么我的列表(列表类型为自定义的类)不能显示在inspector当中
|
||||
- 如何实现的枚举类型在inspector的多选,他的内部机制是什么?
|
||||
- 屏幕数值大小由摄像机决定,摄像机size为6,则高度为12,长度由宽高比可计算得出。
|
||||
### 过程
|
||||
设置地图配置表的过程相对简单,我们仍然需要使用scriptableObject这项技术。创建配置表的过程中,我们建立一个类,这个类的内容是我们每一列房间的数量最大值最小值,以便于我们可以随机生成,同时还需要有我们的房间类型,然后我们使用列表,使用新建立的类为类型,列表的每一个成员都代表了一列房间,一共7列房间,每一列都需要有他可以承载的最大值和最小值,以及他可以包含的房间类型,并且是可以多选的房间类型。
|
||||
完成上述工作之后,我们建立一个配置文件,将每一列的配置信息都做好就完成了本项配置表。
|
||||
@@ -0,0 +1,37 @@
|
||||
这节课在点击回合转换按钮,清空卡牌的时候出现的错误,当时的代码是
|
||||
```
|
||||
public void OnPlayerTurnEnd()
|
||||
{
|
||||
for (int i = 0; i < handCardObjectList.Count; i++)
|
||||
{
|
||||
DiscardDeck(handCardObjectList[i]);
|
||||
}
|
||||
handCardObjectList.Clear();
|
||||
|
||||
}
|
||||
```
|
||||
由于我们使用DiscardDeck函数,每次都会将手牌列表(handCardObjectList)的第一位移除,这样会导致我们的部分卡牌不能正确的被移除.
|
||||
以下是Deepseek的解释.
|
||||
在`OnPlayerTurnEnd`方法中,当遍历手牌列表并调用`DiscardDeck`时,正向循环会导致部分卡牌未被处理。每次移除元素后,列表的索引和长度发生变化,导致后续元素被跳过。以下是解决方案:
|
||||
|
||||
**步骤解释:**
|
||||
|
||||
1. **问题分析:** 正向遍历列表时,移除元素会导致后续索引错位,部分卡牌未被处理。
|
||||
|
||||
2. **解决方案:** 改为从后向前遍历列表,这样移除元素不会影响未处理的索引。
|
||||
|
||||
3. **代码调整:** 修改循环结构,确保所有卡牌都被正确弃用。
|
||||
|
||||
修改后的代码为:
|
||||
```
|
||||
public void OnPlayerTurnEnd()
|
||||
{
|
||||
// 从后向前遍历,避免索引错位
|
||||
for (int i = handCardObjectList.Count - 1; i >= 0; i--)
|
||||
{
|
||||
DiscardDeck(handCardObjectList[i]);
|
||||
}
|
||||
// 清空列表(此时应已为空,但确保万无一失)
|
||||
handCardObjectList.Clear();
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
首先在玩家类当中创建一个蓝条,并且创建一个int variable类型的变量,名字为Player Mana.
|
||||
这个类型在每次蓝条有消耗的时候都会调用事件.
|
||||
因此我们创建一个新的时间,在游戏面板中监听,当数值变化的时候,修改我们游戏面板当中的数值.
|
||||
|
||||
我们打出一张卡牌会有消耗,我们创建一个int variable类型的事件,在打出卡牌的时候,raise这个事件,然后我们在Player当中监听,减去这个消耗,更新蓝条.
|
||||
@@ -0,0 +1,23 @@
|
||||
这节课我们要做的内容是,在玩家回合的时候,敌人会显示他的意图,在敌人回合的时候,敌人会执行.
|
||||
|
||||
我们现在说如何在玩家回合,敌人显示他的意图.
|
||||
|
||||
首先敌人的意图应该是有多个的,我们要在众多意图中随机抽取一个,所以我们就需要建立一个列表,提前在列表中写明他的意图,我们随便抽取一个.
|
||||
|
||||
敌人的意图也就是一个效果Effect,但是我们还要将他显示出来,因此这个列表还需要一个图标,所以我们建立一个结构体struct,他包含一个图标和一个Effect.
|
||||
|
||||
然后我们建立一个SO文件,用来储存这个列表.
|
||||
|
||||
建立之后我们去到Unity当中对这个SO进行设置,并且在Enemy中设置声明获取这个SO文件.
|
||||
|
||||
在Enemy当中我们需要做一件事,就是在玩家回合开始的时候,我们需要在SO中随机抽取一个意图,并且设置一个变量CurrentAciton储存他.
|
||||
|
||||
然后来到血条的代码当中,我们设置一下敌人意图的图标和数字.并且在代码中获取他,将他初始化的时候关闭显示.
|
||||
|
||||
|
||||
我们创建一个新的代码,获取敌人身上的当前意图,将这个意图的图标和数字设置在血条上,当玩家回合开始的时候调用这个函数.
|
||||
|
||||
做完这一切我们在玩家回合的时候就可以显示敌人的意图了,但是我们还没有执行他,我们现在来设置如何在敌人回合执行这些意图.
|
||||
|
||||
我们在Enemy当中创建一个新的方法,他在敌人回合开始的时候调用,他的内容是检测这个意图是对自己还是对敌人,然后执行相应的逻辑.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
首先在character中我们需要声明好一个事件,当玩家死亡的时候调用这个事件.
|
||||
这个事件监听的函数在GameManager当中,我们检测是玩家死了还是敌人死了,玩家死了就执行玩家死了的逻辑,敌人死了就执行敌人死了的逻辑.
|
||||
|
||||
然后我们创建两个事件他们分别是游戏胜利,游戏失败的事件.我们需要这两个事件来启动相应的逻辑.
|
||||
@@ -0,0 +1,7 @@
|
||||
生成房间基本上没有疑问点,重点难点都是数学问题,要看个人如何思考。
|
||||
首先我们要创建一个空物体,创建一个生成房间的脚本挂载在上面,现在我们重点考虑脚本如何写,房间的生成非常简单使用Instantiate实例化预制体即可,关键点在于房间的位置如何计算。
|
||||
我们只需要确定第一列第一个房间的位置,第一列的剩余房间可以加上间隔去确定,其余列也可以加上水平间隔去确定。
|
||||
我们先确定第一个房间的位置,在确定水平间隔以及垂直间隔。
|
||||
第一个房间的x坐标我们可以这样计算,-screenwidth/2,得到最左边的坐标位置.我们加上一个border,得到第一个房间的x坐标,然后用同样的方式计算y坐标.
|
||||
screenheight/2得到最上边的位置,我们在减去screenheight/(amount+1),得到了我们的第一列第一个房间的位置.
|
||||
我们在每次大循环中加上X坐标间隔,在每次小循环中加上y坐标间隔就可以实现房间的生成.
|
||||
@@ -0,0 +1,5 @@
|
||||
生成地图上的连线,并没有什么疑惑点,操作也比较简单.
|
||||
我们创建一个空对象,添加上line renderer组件,设置其大小,将其设置为预制体,在后面通过代码设置他的起始点和最终点.
|
||||
我们的连线规则是,两列之间随意连线,确保每一个房间被连接以及连接出去,实现这样的方式是通过一个foreach循环,将第一列的每一个房间都随机的连接到第二列的某一个房间,当然这样会有问题,第二列的某个房间可能没有被连接到,所以我们需要通过使用HashSet列表,这个列表不会储存重复的成员,每次第二列连接到一个房间,我们就将该房间加入这个列表,列表外的房间就是没有连接的房间,我们将没有连接的第二列的房间,反向连接到第一列即可.
|
||||
然后就是需要对连线的材质进行处理,我们想让连线动起来,我们创立一个unlit的材质,他表示无灯光的材质,也就是可以显示透明的材质,将我们的line材质拖进去,通过脚本调整材质球的offset实现动画效果.
|
||||
我们创建一个line脚本挂载到lineprefab上,通过offset不断增加实现动画效果.
|
||||
@@ -0,0 +1,22 @@
|
||||
### 疑问
|
||||
- addressable场景有什么作用?
|
||||
- 什么是字典?
|
||||
- 场景激活有什么作用?
|
||||
- Enum.Parse是什么?有什么作用?
|
||||
### 过程
|
||||
这节课其实是2节内容,第一节内容是设置每个地图,第二个内容是设置房间的随机类型.
|
||||
我们房间的随机类型的实现过程如下:
|
||||
|
||||
首先我们每一列都储存在mapBluePrints列表当中,每个列表成员有当前列表可包含的所有房类型,我们要做的就是从当前列可以允许的类型当中随机出来一个类型赋值給我们的房间.
|
||||
|
||||
这样做首先暴露出来的问题是,我们随机一个类型,类型是一个代号,他并不包含数据,我们包含数据的成员是我们的RoomDataSO,所以我们要让这两者产生联系,通过字典的键值对.每一个RoomType都要和他的值RoomData相联系.
|
||||
|
||||
我们建立一个RoomDataSo类型的列表,将所有的房间RoomData都放进去,然后通过foreach语句一一连接.
|
||||
|
||||
做好了上一步之后,我们只需要在当前列的类型范围当中,随机一个类型,通过字典获得roomdata,通过room的setup函数设置类型和图标.
|
||||
|
||||
我们如何在当前列中随机一个房间类型?
|
||||
|
||||
我们的房间类型是可以多选的,所以我们可以将他转换为字符串,然后通过字符串切割成多个字符串,存入一个数组当中,然后在数组中随机选择一个索引值,得到一个随机的字符串,通过Enum.Parse函数,将字符串转换为enum类型的值.
|
||||
|
||||
通过这个随机的roomtpye,连接字典得到这个类型的roomdata,将这个roomdata传入setup函数就可以实现随机地图的产生.
|
||||
@@ -0,0 +1,148 @@
|
||||
## 疑问
|
||||
### 1.[TextArea]有什么作用?
|
||||
`[TextArea]` 是一个用于 **增强字符串字段编辑体验** 的重要特性,他的作用是:
|
||||
- 将普通的单行输入框变为 **可伸缩的多行文本区域**
|
||||
- 支持 **回车换行** 和 **文本滚动条**
|
||||
- 默认显示 3 行(可自定义最小/最大行数)
|
||||
2.BaseEventSO当中为什么不使用UnityEvent而是用UnityAction.
|
||||
![[Pasted image 20250222185154.png]]
|
||||
# **过程**
|
||||
|
||||
## Unity 事件系统框架解析(广播-监听模式)
|
||||
|
||||
## 系统架构图
|
||||
|
||||
```markdown
|
||||
[按钮对象] --广播--> [ObjectEventSO] --通知--> [门对象]
|
||||
| |
|
||||
|-- 触发按压动作 |-- 作为事件通道
|
||||
|-- 连接监听器
|
||||
```
|
||||
|
||||
## 一、核心组件说明
|
||||
|
||||
### 1. 广播系统 (EventSO)
|
||||
|
||||
#### 1.1 BaseEventSO<T> 基类
|
||||
|
||||
```csharp
|
||||
// 核心功能:管理事件订阅和广播机制
|
||||
public class BaseEventSO<T> : ScriptableObject
|
||||
{
|
||||
[TextArea] // 可视化描述字段
|
||||
public string description;
|
||||
|
||||
// 事件订阅集合(使用object类型保持灵活性)
|
||||
public UnityAction<T> OnEventRaised;
|
||||
|
||||
// 调试信息字段
|
||||
public string lastSender;
|
||||
|
||||
// 事件触发方法
|
||||
public void RaiseEvent(T value, object sender)
|
||||
{
|
||||
OnEventRaised?.Invoke(value);
|
||||
lastSender = sender.ToString(); // 记录最后发送者
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键设计:**
|
||||
|
||||
- 使用 ScriptableObject 实现全局事件通道
|
||||
- 泛型设计支持多种参数类型
|
||||
- lastSender 字段用于调试追踪
|
||||
|
||||
#### 1.2 ObjectEventSO 具体实现
|
||||
|
||||
```csharp
|
||||
// 创建菜单项便于生成资产
|
||||
[CreateAssetMenu(fileName = "ObjectEventSO", menuName = "Event/ObjectEventSO")]
|
||||
public class ObjectEventSO : BaseEventSO<object> { }
|
||||
```
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 需要传递任意类型参数时
|
||||
- 通用对象交互场景
|
||||
- 快速原型开发阶段
|
||||
|
||||
### 2. 监听系统 (EventListener)
|
||||
|
||||
#### 2.1 BaseEventListener<T> 基类
|
||||
|
||||
```csharp
|
||||
public class BaseEventListener<T> : MonoBehaviour
|
||||
{
|
||||
[Header("事件配置")]
|
||||
public BaseEventSO<T> eventSO; // 绑定的事件资产
|
||||
|
||||
[Header("响应事件")]
|
||||
public UnityEvent<T> response; // 可视化配置的响应事件
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if(eventSO != null)
|
||||
eventSO.OnEventRaised += OnEventRaised;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if(eventSO != null)
|
||||
eventSO.OnEventRaised -= OnEventRaised;
|
||||
}
|
||||
|
||||
private void OnEventRaised(T value)
|
||||
{
|
||||
response.Invoke(value); // 转发事件到UnityEvent
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**生命周期管理:**
|
||||
|
||||
- OnEnable:注册监听
|
||||
- OnDisable:注销监听
|
||||
- 自动化的订阅管理防止内存泄漏
|
||||
|
||||
#### 2.2 ObjectEventListener 具体实现
|
||||
|
||||
```csharp
|
||||
public class ObjectEventListener : BaseEventListener<object> { }
|
||||
```
|
||||
|
||||
**扩展说明:**
|
||||
|
||||
- 可创建特定类型的监听器(如IntEventListener)
|
||||
- 保持基类的泛型灵活性
|
||||
|
||||
## 二、工作流程详解
|
||||
|
||||
### 1. 配置阶段
|
||||
|
||||
1. 创建 EventSO 资产
|
||||
- Assets右键 → Create → Event → ObjectEventSO
|
||||
2. 配置广播者(按钮)
|
||||
|
||||
- 添加事件触发脚本
|
||||
|
||||
```csharp
|
||||
public class ButtonPressHandler : MonoBehaviour
|
||||
{
|
||||
public ObjectEventSO pressEvent;
|
||||
|
||||
void OnMouseDown()
|
||||
{
|
||||
pressEvent.RaiseEvent(this, "DoorButton");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 配置监听者(门)
|
||||
- 添加 ObjectEventListener 组件
|
||||
- 绑定同一个 EventSO 资产
|
||||
- 配置 Response 事件链(示例):
|
||||
- 添加 GameObject.SetActive(false)
|
||||
- 添加 Animator.Play("OpenDoor")
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
## 疑问
|
||||
### 1.addressable场景有什么作用?如过不addressable会怎么样?
|
||||
|
||||
|
||||
使用addressable需要引入包
|
||||
|
||||
加载房间也是一个重点和难点,因为他涉及到了我没有学习过的场景加载方面的内容
|
||||
传统的场景异步加载使用的是协程而不是async/await,因为协程是Unity集成的,他的性能比C#的Task差,但是在2023更新之后,将c#的Task更新为了Awaitable,他的性能比C#的Task好得多,因此教程使用了Awaitable来进行场景加载,这节笔记首先讲解课程所使用的方法,在讲解协程的老方法.
|
||||
|
||||
同时我在刚开始对Task非常疑惑,Task类型其实就是一个异步操作,我们在使用async/await的时候需要将其中的方法设置为Task,在Unity更新之后,我们可以用Awaitable代替Task.
|
||||
|
||||
协程是使用yield来实现异步的等待,Awaitable是使用await来实现异步的等待.
|
||||
|
||||
# **使用Awaitable实现场景切换**
|
||||
|
||||
本节课加载房间的流程:首先创建我们所有需要的房间并且将他们设置为Addressable,然后在
|
||||
Windows->Asset Management->Addressables->Groups当中简化我们房间的名字.
|
||||
我们需要在场景中创建一个空对象命名为SceneLoadManager用来控制我们的场景加载,在类中我们首先声明两个变量,他们的类型是AssetReference,一个名为currentScene一个为map,标明一个是需要加载的场景,另一个是地图场景.
|
||||
我们目前处于地图场景当中,当我们点击了房间的图标的加载进入图标的房间,当我们点击房间的时候通过事件会将当前房间的数据传入到我们这个类当中,所以我们可以知道我们需要加载的场景是什么,同时将我们的currentScene赋值为当前需要加载的场景,然后我们卸载Map,加载currentScene,同时将他设置为setactiveScene.
|
||||
|
||||
加载房间和卸载房间的方法,首先他的类型是async Awaitable,这是2023以后推出.
|
||||
加载房间中我需要异步加载当前场景
|
||||
```
|
||||
private async Awaitable LoadSceneTask()
|
||||
|
||||
{
|
||||
|
||||
var s = currentScene.LoadSceneAsync(LoadSceneMode.Additive);
|
||||
|
||||
await s.Task;
|
||||
|
||||
if (s.Status == AsyncOperationStatus.Succeeded)
|
||||
|
||||
{
|
||||
|
||||
SceneManager.SetActiveScene(s.Result.Scene);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
```
|
||||
我们通过AssetReference.LoadSceneAsync,加载场景, await s.Task等待加载完成,然后通过一个if语句判断,如过加载成功,就将当前场景切换为currentScene
|
||||
```
|
||||
private async Awaitable UnLoadSceneTask()
|
||||
|
||||
{
|
||||
|
||||
await SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene());
|
||||
|
||||
}
|
||||
```
|
||||
卸载场景也是一样的.
|
||||
在本教程当中,由于设置了一个持久化场景,我们其余的Map,Room场景都是第二第三个场景,并且每一个场景都包含很多的东西,因此我们在加载场景的过程中需要先卸载,在加载场景.
|
||||
|
||||
## **使用协程进行场景切换**
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
保存地图场景,顾名思义,他的本质就是保存,将我们的房间数据保存到另一个物体当中,并且我们可以从这个物体中读取数据.
|
||||
|
||||
我们已经使用过了scriptableobject,知道他是一种数据容器,可以储存和保存数据,因此考虑使用SO文件来储存地图数据.
|
||||
|
||||
我们需要保存的地图数据只有2个,第一个是房间,第二是连线.地图当中有非常非常多的房间和连线,所以我们肯定会使用列表来储存,但是没有一种类型可以支持我们储存这些数据,因此我们需要创建一个自定义的类来储存房间和连线.
|
||||
|
||||
我们先考虑一下储存房间需要哪些信息,他的行和列,他的X.Y坐标,以及他的roomData数据和RoomState.那我们的连线呢?只需要保存他的起始坐标和终止坐标就可以了.虽然我们的游戏是2D的,但是连线的位置坐标却是3D的,vector3类型的无法被序列化,我们需要创建一个可以帮助v3序列化的工具类.
|
||||
|
||||
```
|
||||
using UnityEngine;
|
||||
|
||||
[System.Serializable]
|
||||
|
||||
public class serializeVector3
|
||||
|
||||
{
|
||||
|
||||
public float x, y, z;
|
||||
|
||||
public serializeVector3(Vector3 vector3)
|
||||
|
||||
{
|
||||
|
||||
x = vector3.x;
|
||||
|
||||
y = vector3.y;
|
||||
|
||||
z = vector3.z;
|
||||
|
||||
}
|
||||
|
||||
public Vector3 ToVector3()
|
||||
|
||||
{
|
||||
|
||||
return new Vector3(x, y, z);
|
||||
|
||||
}
|
||||
|
||||
public Vector2 ToVector2Int()
|
||||
|
||||
{
|
||||
|
||||
return new Vector2((int)x, (int)y);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
这个类可以帮助我们序列化vector3.
|
||||
在想好了上述内容之后,我们创建MapLayout,他的代码如下
|
||||
|
||||
```
|
||||
public class MapLayoutSO : ScriptableObject
|
||||
|
||||
{
|
||||
|
||||
//创建两个列表,一个储存房间信息,一个储存连线信息
|
||||
|
||||
public List<MapRoomData> roomDataList = new List<MapRoomData>();
|
||||
|
||||
public List<MapLineData> lineDataList = new List<MapLineData>();
|
||||
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
|
||||
public class MapRoomData
|
||||
|
||||
{
|
||||
|
||||
//我们要保存的房间的信息
|
||||
|
||||
public float PosX, PosY;
|
||||
|
||||
public int column, line;
|
||||
|
||||
public RoomDataSO roomData;
|
||||
|
||||
public RoomState roomState;
|
||||
|
||||
public List<Vector2Int> linkTo;
|
||||
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
|
||||
public class MapLineData
|
||||
|
||||
{
|
||||
|
||||
//连线我们只需要知道他的起始坐标和结束坐标但是这些内容是vector3类型的
|
||||
|
||||
// vector3类型的无法被序列化,我们需要创建一个可以帮助v3序列化的工具类
|
||||
|
||||
public serializeVector3 startPos, endPos;
|
||||
|
||||
}
|
||||
```
|
||||
那么后面我们需要做的,是从rooms这个列表当中,循环读取,将他的内容储存到我们的SO文件当中.
|
||||
同时在创建地图的时候增加一层判断,如过我们的SO文件当中有数据,那么读取SO文件的数据,而不是从新创建地图.
|
||||
@@ -0,0 +1,8 @@
|
||||
1.连线信息起始坐标没有被正确保存序列化
|
||||
serializeVector3类不能继承MonoBehaviour.这会导致序列化完全失效.
|
||||
- MonoBehaviour 是 Unity 组件的基类,必须依附于 GameObject 存在
|
||||
- ScriptableObject 只能序列化 **纯数据类**(不继承 MonoBehaviour 的类)
|
||||
我由于给serializeVector3继承了MonoBehaviour,导致了我的连线保存以及我的maplayout当中并不能正确的序列化连线的坐标信息导致出现了问题.
|
||||
![[Pasted image 20250222164926.png]]
|
||||
|
||||
![[Pasted image 20250222164957.png]]
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,17 @@
|
||||
#AI
|
||||
#配置
|
||||
|
||||
1.准备node.js
|
||||
2.准备git
|
||||
3.安装node.js和git
|
||||
4.windows平台在powershell输入
|
||||
iwr -useb https://openclaw.ai/install.ps1 | iex
|
||||
来安装小龙虾
|
||||
5.openclaw onboard --install-daemon
|
||||
输入指令进入新手引导
|
||||
6.openclaw gateway
|
||||
启动小龙虾
|
||||
|
||||
还需要注意我们的api获取渠道
|
||||
让用户登录https://platform.moonshot.cn/
|
||||
进行实名注册,获得15元API额度。
|
||||