feat: add event log panel, Chinese font support, and documentation
- Add EventLog.cs: scrollable event log panel showing game events - Color-coded entries for different event types - Toggle visibility, clear button, timestamps - Auto-scroll to newest entries - Add Chinese font support (Source Han Sans SC) - SourceHanSansSC-Regular.otf font file - ChineseFontSetup.cs editor tool for TMP font asset generation - Configure as fallback font for TextMeshPro - Fix particle system velocity curve warnings in WeatherEffects.cs - Ensure all velocity axes use consistent curve mode - Add comprehensive README.md with project documentation - Architecture overview, features, tech stack - Quick start guide, Unity client structure - Communication protocol, development notes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
176
README.md
Normal file
176
README.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# The Island - 荒岛生存模拟游戏
|
||||||
|
|
||||||
|
一个实时多人互动的荒岛生存模拟游戏,玩家可以通过命令与 AI 角色互动,帮助他们在荒岛上生存。
|
||||||
|
|
||||||
|
## 项目架构
|
||||||
|
|
||||||
|
```
|
||||||
|
the-island/
|
||||||
|
├── backend/ # Python FastAPI 后端服务
|
||||||
|
│ └── app/
|
||||||
|
│ ├── main.py # 应用入口
|
||||||
|
│ ├── server.py # WebSocket 服务器
|
||||||
|
│ ├── engine.py # 游戏引擎核心逻辑
|
||||||
|
│ ├── models.py # SQLAlchemy 数据模型
|
||||||
|
│ ├── schemas.py # Pydantic 消息模式
|
||||||
|
│ ├── llm.py # LLM 集成 (对话生成)
|
||||||
|
│ └── database.py # 数据库配置
|
||||||
|
├── frontend/ # Web 调试客户端
|
||||||
|
│ ├── app.js # JavaScript 客户端
|
||||||
|
│ └── debug_client.html # 调试页面
|
||||||
|
├── unity-client/ # Unity 6 游戏客户端
|
||||||
|
│ └── Assets/
|
||||||
|
│ ├── Scripts/ # C# 游戏脚本
|
||||||
|
│ ├── Fonts/ # 字体资源 (含中文支持)
|
||||||
|
│ └── Editor/ # 编辑器工具
|
||||||
|
└── island.db # SQLite 数据库
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 游戏系统
|
||||||
|
- **生存机制**: 角色有 HP、能量、心情三大属性
|
||||||
|
- **昼夜循环**: 黎明 → 白天 → 黄昏 → 夜晚
|
||||||
|
- **天气系统**: 晴天、多云、雨天、暴风雨、炎热、雾天
|
||||||
|
- **社交系统**: 角色间自主社交互动
|
||||||
|
- **休闲模式**: 自动复活、降低难度
|
||||||
|
|
||||||
|
### 玩家命令
|
||||||
|
| 命令 | 格式 | 金币消耗 | 效果 |
|
||||||
|
|------|------|----------|------|
|
||||||
|
| feed | `feed <角色名>` | 10g | +20 能量, +5 HP |
|
||||||
|
| heal | `heal <角色名>` | 15g | +30 HP |
|
||||||
|
| talk | `talk <角色名> [话题]` | 0g | 与角色对话 |
|
||||||
|
| encourage | `encourage <角色名>` | 5g | +15 心情 |
|
||||||
|
| revive | `revive <角色名>` | 10g | 复活死亡角色 |
|
||||||
|
| check | `check` | 0g | 查看所有状态 |
|
||||||
|
| reset | `reset` | 0g | 重置游戏 |
|
||||||
|
|
||||||
|
### AI 角色
|
||||||
|
- **Jack** (勇敢) - 蓝色
|
||||||
|
- **Luna** (狡猾) - 粉色
|
||||||
|
- **Bob** (诚实) - 绿色
|
||||||
|
|
||||||
|
每个角色有独特性格,会根据性格做出不同反应和社交行为。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- **Python 3.11+**
|
||||||
|
- **FastAPI** - 异步 Web 框架
|
||||||
|
- **WebSocket** - 实时双向通信
|
||||||
|
- **SQLAlchemy** - ORM 数据持久化
|
||||||
|
- **SQLite** - 轻量级数据库
|
||||||
|
- **Anthropic Claude** - LLM 对话生成
|
||||||
|
|
||||||
|
### Unity 客户端
|
||||||
|
- **Unity 6 LTS** (6000.3.2f1)
|
||||||
|
- **TextMeshPro** - 高质量文本渲染
|
||||||
|
- **NativeWebSocket** - WebSocket 通信
|
||||||
|
- **2.5D 风格** - 精灵 + Billboard UI
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 启动后端服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启动 Unity 客户端
|
||||||
|
|
||||||
|
1. 使用 Unity 6 打开 `unity-client` 文件夹
|
||||||
|
2. 打开 `Assets/Scenes/main.unity`
|
||||||
|
3. 点击 Play 运行游戏
|
||||||
|
|
||||||
|
### 3. Web 调试客户端 (可选)
|
||||||
|
|
||||||
|
在浏览器打开 `frontend/debug_client.html`
|
||||||
|
|
||||||
|
## Unity 客户端结构
|
||||||
|
|
||||||
|
### 核心脚本
|
||||||
|
| 脚本 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `NetworkManager.cs` | WebSocket 连接管理、消息收发 |
|
||||||
|
| `GameManager.cs` | 游戏状态管理、角色生成 |
|
||||||
|
| `UIManager.cs` | 主 UI 界面 (顶部状态栏、底部命令输入) |
|
||||||
|
| `EventLog.cs` | 事件日志面板 (显示游戏事件) |
|
||||||
|
| `AgentVisual.cs` | 角色视觉组件 (精灵、血条、对话框) |
|
||||||
|
| `EnvironmentManager.cs` | 环境场景 (沙滩、海洋、天空) |
|
||||||
|
| `WeatherEffects.cs` | 天气粒子效果 (雨、雾、热浪) |
|
||||||
|
|
||||||
|
### 视觉特性
|
||||||
|
- 程序化生成的 2.5D 角色精灵
|
||||||
|
- Billboard UI (始终面向摄像机)
|
||||||
|
- 动态天气粒子系统
|
||||||
|
- 渐变天空盒 (随时间变化)
|
||||||
|
- 海浪动画效果
|
||||||
|
|
||||||
|
## 中文字体支持
|
||||||
|
|
||||||
|
项目使用 **思源黑体 (Source Han Sans SC)** 支持中文显示。
|
||||||
|
|
||||||
|
### 手动配置步骤
|
||||||
|
1. 选择 `Assets/Fonts/SourceHanSansSC-Regular.otf`
|
||||||
|
2. 右键 → Create → TextMeshPro → Font Asset → SDF
|
||||||
|
3. 打开 Edit → Project Settings → TextMesh Pro
|
||||||
|
4. 在 Fallback Font Assets 中添加生成的字体资产
|
||||||
|
|
||||||
|
## 通信协议
|
||||||
|
|
||||||
|
### WebSocket 端点
|
||||||
|
```
|
||||||
|
ws://localhost:8000/ws/{username}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 事件类型
|
||||||
|
```python
|
||||||
|
# 核心事件
|
||||||
|
TICK # 游戏心跳
|
||||||
|
AGENTS_UPDATE # 角色状态更新
|
||||||
|
AGENT_SPEAK # 角色发言
|
||||||
|
AGENT_DIED # 角色死亡
|
||||||
|
|
||||||
|
# 时间系统
|
||||||
|
PHASE_CHANGE # 时段变化 (黎明/白天/黄昏/夜晚)
|
||||||
|
DAY_CHANGE # 新的一天
|
||||||
|
WEATHER_CHANGE # 天气变化
|
||||||
|
|
||||||
|
# 玩家互动
|
||||||
|
FEED # 喂食反馈
|
||||||
|
HEAL # 治疗反馈
|
||||||
|
TALK # 对话反馈
|
||||||
|
ENCOURAGE # 鼓励反馈
|
||||||
|
REVIVE # 复活反馈
|
||||||
|
|
||||||
|
# 社交系统
|
||||||
|
SOCIAL_INTERACTION # 角色间社交
|
||||||
|
AUTO_REVIVE # 自动复活 (休闲模式)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
创建 `.env` 文件:
|
||||||
|
```env
|
||||||
|
ANTHROPIC_API_KEY=your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
### 添加新命令
|
||||||
|
1. 在 `backend/app/schemas.py` 添加事件类型
|
||||||
|
2. 在 `backend/app/engine.py` 添加命令处理逻辑
|
||||||
|
3. 在 `unity-client/Assets/Scripts/Models.cs` 添加数据模型
|
||||||
|
4. 在 `unity-client/Assets/Scripts/NetworkManager.cs` 添加事件处理
|
||||||
|
|
||||||
|
### 调试
|
||||||
|
- Unity 控制台查看日志
|
||||||
|
- Web 调试客户端查看原始消息
|
||||||
|
- 后端日志查看服务器状态
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
227
unity-client/Assets/Editor/ChineseFontSetup.cs
Normal file
227
unity-client/Assets/Editor/ChineseFontSetup.cs
Normal file
File diff suppressed because one or more lines are too long
2
unity-client/Assets/Editor/ChineseFontSetup.cs.meta
Normal file
2
unity-client/Assets/Editor/ChineseFontSetup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 21528455f756640db912f845f44864a6
|
||||||
8
unity-client/Assets/Fonts.meta
Normal file
8
unity-client/Assets/Fonts.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 18b316c9d4e944c9ead9102780fda528
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
19872
unity-client/Assets/Fonts/SourceHanSansSC-Regular SDF.asset
Normal file
19872
unity-client/Assets/Fonts/SourceHanSansSC-Regular SDF.asset
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 05d7311017e2a4618aeb1598563375bc
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
unity-client/Assets/Fonts/SourceHanSansSC-Regular.otf
Normal file
BIN
unity-client/Assets/Fonts/SourceHanSansSC-Regular.otf
Normal file
Binary file not shown.
21
unity-client/Assets/Fonts/SourceHanSansSC-Regular.otf.meta
Normal file
21
unity-client/Assets/Fonts/SourceHanSansSC-Regular.otf.meta
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ce5da230aa9d2439c998f40a36e72588
|
||||||
|
TrueTypeFontImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 4
|
||||||
|
fontSize: 16
|
||||||
|
forceTextureCase: -2
|
||||||
|
characterSpacing: 0
|
||||||
|
characterPadding: 1
|
||||||
|
includeFontData: 1
|
||||||
|
fontNames:
|
||||||
|
- Source Han Sans SC
|
||||||
|
fallbackFontReferences: []
|
||||||
|
customCharacters:
|
||||||
|
fontRenderingMode: 0
|
||||||
|
ascentCalculationMode: 1
|
||||||
|
useLegacyBoundsCalculation: 0
|
||||||
|
shouldRoundAdvanceValue: 1
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
379
unity-client/Assets/Scripts/EventLog.cs
Normal file
379
unity-client/Assets/Scripts/EventLog.cs
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using TMPro;
|
||||||
|
using TheIsland.Network;
|
||||||
|
using TheIsland.Models;
|
||||||
|
|
||||||
|
namespace TheIsland.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 事件日志面板 - 显示游戏事件历史
|
||||||
|
/// </summary>
|
||||||
|
public class EventLog : MonoBehaviour
|
||||||
|
{
|
||||||
|
private static EventLog _instance;
|
||||||
|
public static EventLog Instance => _instance;
|
||||||
|
|
||||||
|
// 自动创建 EventLog 实例
|
||||||
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||||
|
private static void AutoCreate()
|
||||||
|
{
|
||||||
|
if (_instance == null)
|
||||||
|
{
|
||||||
|
var go = new GameObject("EventLog");
|
||||||
|
go.AddComponent<EventLog>();
|
||||||
|
DontDestroyOnLoad(go);
|
||||||
|
Debug.Log("[EventLog] 自动创建实例");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI 组件
|
||||||
|
private Canvas _canvas;
|
||||||
|
private GameObject _panel;
|
||||||
|
private ScrollRect _scrollRect;
|
||||||
|
private RectTransform _content;
|
||||||
|
private Button _toggleBtn;
|
||||||
|
private TextMeshProUGUI _toggleText;
|
||||||
|
private List<GameObject> _entries = new List<GameObject>();
|
||||||
|
private bool _visible = true;
|
||||||
|
private int _unread = 0;
|
||||||
|
private bool _ready = false;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (_instance != null && _instance != this)
|
||||||
|
{
|
||||||
|
Destroy(gameObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
Debug.Log("[EventLog] Start - 开始初始化");
|
||||||
|
StartCoroutine(InitCoroutine());
|
||||||
|
}
|
||||||
|
|
||||||
|
private System.Collections.IEnumerator InitCoroutine()
|
||||||
|
{
|
||||||
|
// 等待一帧确保 Canvas 准备好
|
||||||
|
yield return null;
|
||||||
|
|
||||||
|
_canvas = FindAnyObjectByType<Canvas>();
|
||||||
|
if (_canvas == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[EventLog] 找不到 Canvas");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log($"[EventLog] 找到 Canvas: {_canvas.name}");
|
||||||
|
BuildUI();
|
||||||
|
|
||||||
|
// 等待 NetworkManager
|
||||||
|
int retries = 0;
|
||||||
|
while (NetworkManager.Instance == null && retries < 20)
|
||||||
|
{
|
||||||
|
Debug.Log("[EventLog] 等待 NetworkManager...");
|
||||||
|
yield return new WaitForSeconds(0.25f);
|
||||||
|
retries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NetworkManager.Instance != null)
|
||||||
|
{
|
||||||
|
SubscribeEvents();
|
||||||
|
_ready = true;
|
||||||
|
AddLog("事件日志已就绪", Color.yellow);
|
||||||
|
Debug.Log("[EventLog] 初始化完成");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError("[EventLog] NetworkManager 超时");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (NetworkManager.Instance != null)
|
||||||
|
{
|
||||||
|
var n = NetworkManager.Instance;
|
||||||
|
n.OnConnected -= OnConnected;
|
||||||
|
n.OnAgentSpeak -= OnSpeak;
|
||||||
|
n.OnSocialInteraction -= OnSocial;
|
||||||
|
n.OnAgentDied -= OnDeath;
|
||||||
|
n.OnFeed -= OnFeed;
|
||||||
|
n.OnHeal -= OnHeal;
|
||||||
|
n.OnEncourage -= OnEncourage;
|
||||||
|
n.OnRevive -= OnRevive;
|
||||||
|
n.OnTalk -= OnTalk;
|
||||||
|
n.OnSystemMessage -= OnSystem;
|
||||||
|
n.OnWeatherChange -= OnWeather;
|
||||||
|
n.OnPhaseChange -= OnPhase;
|
||||||
|
n.OnDayChange -= OnDay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubscribeEvents()
|
||||||
|
{
|
||||||
|
var n = NetworkManager.Instance;
|
||||||
|
n.OnConnected += OnConnected;
|
||||||
|
n.OnAgentSpeak += OnSpeak;
|
||||||
|
n.OnSocialInteraction += OnSocial;
|
||||||
|
n.OnAgentDied += OnDeath;
|
||||||
|
n.OnFeed += OnFeed;
|
||||||
|
n.OnHeal += OnHeal;
|
||||||
|
n.OnEncourage += OnEncourage;
|
||||||
|
n.OnRevive += OnRevive;
|
||||||
|
n.OnTalk += OnTalk;
|
||||||
|
n.OnSystemMessage += OnSystem;
|
||||||
|
n.OnWeatherChange += OnWeather;
|
||||||
|
n.OnPhaseChange += OnPhase;
|
||||||
|
n.OnDayChange += OnDay;
|
||||||
|
Debug.Log("[EventLog] 事件订阅完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件处理
|
||||||
|
private void OnConnected() => AddLog("已连接服务器", Color.green);
|
||||||
|
private void OnSpeak(AgentSpeakData d) => AddLog($"{d.agent_name}: \"{d.text}\"", Color.white);
|
||||||
|
private void OnSocial(SocialInteractionData d) => AddLog($"{d.initiator_name} -> {d.target_name}: {d.dialogue}", new Color(0.6f, 0.9f, 1f));
|
||||||
|
private void OnDeath(AgentDiedData d) => AddLog($"{d.agent_name} 死亡!", Color.red);
|
||||||
|
private void OnFeed(FeedEventData d) => AddLog($"{d.user} 喂食 {d.agent_name}", new Color(0.5f, 1f, 0.5f));
|
||||||
|
private void OnHeal(HealEventData d) => AddLog($"{d.user} 治疗 {d.agent_name}", new Color(0.5f, 1f, 0.5f));
|
||||||
|
private void OnEncourage(EncourageEventData d) => AddLog($"{d.user} 鼓励 {d.agent_name}", new Color(0.5f, 1f, 0.5f));
|
||||||
|
private void OnRevive(ReviveEventData d) => AddLog($"{d.user} 复活 {d.agent_name}", Color.cyan);
|
||||||
|
private void OnTalk(TalkEventData d) => AddLog($"{d.agent_name}: \"{d.response}\"", Color.white);
|
||||||
|
private void OnSystem(SystemEventData d) => AddLog(d.message, Color.yellow);
|
||||||
|
private void OnWeather(WeatherChangeData d) => AddLog($"天气: {d.new_weather}", new Color(0.7f, 0.85f, 1f));
|
||||||
|
private void OnPhase(PhaseChangeData d) => AddLog($"时间: {d.new_phase}", new Color(1f, 0.9f, 0.7f));
|
||||||
|
private void OnDay(DayChangeData d) => AddLog($"第 {d.day} 天!", new Color(1f, 0.9f, 0.7f));
|
||||||
|
|
||||||
|
private void BuildUI()
|
||||||
|
{
|
||||||
|
// 切换按钮
|
||||||
|
var toggleObj = new GameObject("LogToggle");
|
||||||
|
toggleObj.transform.SetParent(_canvas.transform, false);
|
||||||
|
var toggleRect = toggleObj.AddComponent<RectTransform>();
|
||||||
|
toggleRect.anchorMin = new Vector2(0, 1);
|
||||||
|
toggleRect.anchorMax = new Vector2(0, 1);
|
||||||
|
toggleRect.pivot = new Vector2(0, 1);
|
||||||
|
toggleRect.anchoredPosition = new Vector2(10, -75);
|
||||||
|
toggleRect.sizeDelta = new Vector2(90, 24);
|
||||||
|
|
||||||
|
var toggleImg = toggleObj.AddComponent<Image>();
|
||||||
|
toggleImg.color = new Color(0.2f, 0.35f, 0.5f, 0.9f);
|
||||||
|
_toggleBtn = toggleObj.AddComponent<Button>();
|
||||||
|
_toggleBtn.targetGraphic = toggleImg;
|
||||||
|
_toggleBtn.onClick.AddListener(Toggle);
|
||||||
|
|
||||||
|
var toggleTextObj = new GameObject("Text");
|
||||||
|
toggleTextObj.transform.SetParent(toggleObj.transform, false);
|
||||||
|
_toggleText = toggleTextObj.AddComponent<TextMeshProUGUI>();
|
||||||
|
_toggleText.text = "隐藏日志";
|
||||||
|
_toggleText.fontSize = 12;
|
||||||
|
_toggleText.color = Color.white;
|
||||||
|
_toggleText.alignment = TextAlignmentOptions.Center;
|
||||||
|
var ttRect = toggleTextObj.GetComponent<RectTransform>();
|
||||||
|
ttRect.anchorMin = Vector2.zero;
|
||||||
|
ttRect.anchorMax = Vector2.one;
|
||||||
|
ttRect.sizeDelta = Vector2.zero;
|
||||||
|
|
||||||
|
// 主面板
|
||||||
|
_panel = new GameObject("LogPanel");
|
||||||
|
_panel.transform.SetParent(_canvas.transform, false);
|
||||||
|
var panelRect = _panel.AddComponent<RectTransform>();
|
||||||
|
panelRect.anchorMin = new Vector2(0, 0);
|
||||||
|
panelRect.anchorMax = new Vector2(0, 1);
|
||||||
|
panelRect.pivot = new Vector2(0, 0.5f);
|
||||||
|
panelRect.offsetMin = new Vector2(10, 80);
|
||||||
|
panelRect.offsetMax = new Vector2(360, -80);
|
||||||
|
|
||||||
|
var panelImg = _panel.AddComponent<Image>();
|
||||||
|
panelImg.color = new Color(0.05f, 0.07f, 0.1f, 0.95f);
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
var header = new GameObject("Header");
|
||||||
|
header.transform.SetParent(_panel.transform, false);
|
||||||
|
var headerRect = header.AddComponent<RectTransform>();
|
||||||
|
headerRect.anchorMin = new Vector2(0, 1);
|
||||||
|
headerRect.anchorMax = new Vector2(1, 1);
|
||||||
|
headerRect.pivot = new Vector2(0.5f, 1);
|
||||||
|
headerRect.sizeDelta = new Vector2(0, 28);
|
||||||
|
headerRect.anchoredPosition = Vector2.zero;
|
||||||
|
|
||||||
|
header.AddComponent<Image>().color = new Color(0.12f, 0.15f, 0.2f);
|
||||||
|
|
||||||
|
var titleObj = new GameObject("Title");
|
||||||
|
titleObj.transform.SetParent(header.transform, false);
|
||||||
|
var titleTmp = titleObj.AddComponent<TextMeshProUGUI>();
|
||||||
|
titleTmp.text = "事件日志";
|
||||||
|
titleTmp.fontSize = 14;
|
||||||
|
titleTmp.fontStyle = FontStyles.Bold;
|
||||||
|
titleTmp.color = Color.white;
|
||||||
|
titleTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||||
|
var titleRect = titleObj.GetComponent<RectTransform>();
|
||||||
|
titleRect.anchorMin = Vector2.zero;
|
||||||
|
titleRect.anchorMax = Vector2.one;
|
||||||
|
titleRect.offsetMin = new Vector2(8, 0);
|
||||||
|
titleRect.offsetMax = new Vector2(-50, 0);
|
||||||
|
|
||||||
|
// 清除按钮
|
||||||
|
var clearObj = new GameObject("Clear");
|
||||||
|
clearObj.transform.SetParent(header.transform, false);
|
||||||
|
var clearRect = clearObj.AddComponent<RectTransform>();
|
||||||
|
clearRect.anchorMin = new Vector2(1, 0.5f);
|
||||||
|
clearRect.anchorMax = new Vector2(1, 0.5f);
|
||||||
|
clearRect.pivot = new Vector2(1, 0.5f);
|
||||||
|
clearRect.sizeDelta = new Vector2(45, 20);
|
||||||
|
clearRect.anchoredPosition = new Vector2(-4, 0);
|
||||||
|
|
||||||
|
var clearImg = clearObj.AddComponent<Image>();
|
||||||
|
clearImg.color = new Color(0.5f, 0.25f, 0.25f);
|
||||||
|
var clearBtn = clearObj.AddComponent<Button>();
|
||||||
|
clearBtn.targetGraphic = clearImg;
|
||||||
|
clearBtn.onClick.AddListener(Clear);
|
||||||
|
|
||||||
|
var clearTextObj = new GameObject("Text");
|
||||||
|
clearTextObj.transform.SetParent(clearObj.transform, false);
|
||||||
|
var clearTmp = clearTextObj.AddComponent<TextMeshProUGUI>();
|
||||||
|
clearTmp.text = "清除";
|
||||||
|
clearTmp.fontSize = 11;
|
||||||
|
clearTmp.color = Color.white;
|
||||||
|
clearTmp.alignment = TextAlignmentOptions.Center;
|
||||||
|
var ctRect = clearTextObj.GetComponent<RectTransform>();
|
||||||
|
ctRect.anchorMin = Vector2.zero;
|
||||||
|
ctRect.anchorMax = Vector2.one;
|
||||||
|
ctRect.sizeDelta = Vector2.zero;
|
||||||
|
|
||||||
|
// 滚动区域
|
||||||
|
var scrollObj = new GameObject("Scroll");
|
||||||
|
scrollObj.transform.SetParent(_panel.transform, false);
|
||||||
|
var scrollRect = scrollObj.AddComponent<RectTransform>();
|
||||||
|
scrollRect.anchorMin = Vector2.zero;
|
||||||
|
scrollRect.anchorMax = Vector2.one;
|
||||||
|
scrollRect.offsetMin = new Vector2(4, 4);
|
||||||
|
scrollRect.offsetMax = new Vector2(-4, -32);
|
||||||
|
|
||||||
|
scrollObj.AddComponent<Image>().color = new Color(0, 0, 0, 0.3f);
|
||||||
|
scrollObj.AddComponent<Mask>().showMaskGraphic = true;
|
||||||
|
|
||||||
|
_scrollRect = scrollObj.AddComponent<ScrollRect>();
|
||||||
|
_scrollRect.horizontal = false;
|
||||||
|
_scrollRect.vertical = true;
|
||||||
|
_scrollRect.movementType = ScrollRect.MovementType.Clamped;
|
||||||
|
_scrollRect.scrollSensitivity = 25;
|
||||||
|
_scrollRect.viewport = scrollRect;
|
||||||
|
|
||||||
|
// 内容容器
|
||||||
|
var contentObj = new GameObject("Content");
|
||||||
|
contentObj.transform.SetParent(scrollObj.transform, false);
|
||||||
|
_content = contentObj.AddComponent<RectTransform>();
|
||||||
|
_content.anchorMin = new Vector2(0, 1);
|
||||||
|
_content.anchorMax = new Vector2(1, 1);
|
||||||
|
_content.pivot = new Vector2(0.5f, 1);
|
||||||
|
_content.anchoredPosition = Vector2.zero;
|
||||||
|
_content.sizeDelta = new Vector2(0, 0);
|
||||||
|
|
||||||
|
var layout = contentObj.AddComponent<VerticalLayoutGroup>();
|
||||||
|
layout.spacing = 2;
|
||||||
|
layout.padding = new RectOffset(2, 2, 2, 2);
|
||||||
|
layout.childControlWidth = true;
|
||||||
|
layout.childControlHeight = true;
|
||||||
|
layout.childForceExpandWidth = true;
|
||||||
|
layout.childForceExpandHeight = false;
|
||||||
|
|
||||||
|
contentObj.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||||
|
_scrollRect.content = _content;
|
||||||
|
|
||||||
|
Debug.Log("[EventLog] UI 构建完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddLog(string msg, Color color)
|
||||||
|
{
|
||||||
|
if (_content == null) return;
|
||||||
|
|
||||||
|
var entry = new GameObject($"E{_entries.Count}");
|
||||||
|
entry.transform.SetParent(_content, false);
|
||||||
|
|
||||||
|
entry.AddComponent<Image>().color = _entries.Count % 2 == 0
|
||||||
|
? new Color(0.08f, 0.1f, 0.13f, 0.9f)
|
||||||
|
: new Color(0.06f, 0.08f, 0.11f, 0.9f);
|
||||||
|
|
||||||
|
var le = entry.AddComponent<LayoutElement>();
|
||||||
|
le.minHeight = 36;
|
||||||
|
le.preferredHeight = 36;
|
||||||
|
|
||||||
|
// 颜色条
|
||||||
|
var bar = new GameObject("Bar");
|
||||||
|
bar.transform.SetParent(entry.transform, false);
|
||||||
|
var barRect = bar.AddComponent<RectTransform>();
|
||||||
|
barRect.anchorMin = new Vector2(0, 0);
|
||||||
|
barRect.anchorMax = new Vector2(0, 1);
|
||||||
|
barRect.pivot = new Vector2(0, 0.5f);
|
||||||
|
barRect.sizeDelta = new Vector2(3, -4);
|
||||||
|
barRect.anchoredPosition = new Vector2(1, 0);
|
||||||
|
bar.AddComponent<Image>().color = color;
|
||||||
|
|
||||||
|
// 文本
|
||||||
|
var textObj = new GameObject("Text");
|
||||||
|
textObj.transform.SetParent(entry.transform, false);
|
||||||
|
var tmp = textObj.AddComponent<TextMeshProUGUI>();
|
||||||
|
string time = System.DateTime.Now.ToString("HH:mm:ss");
|
||||||
|
tmp.text = $"<color=#666><size=10>{time}</size></color> {msg}";
|
||||||
|
tmp.fontSize = 12;
|
||||||
|
tmp.color = color;
|
||||||
|
tmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||||
|
tmp.textWrappingMode = TextWrappingModes.Normal;
|
||||||
|
tmp.overflowMode = TextOverflowModes.Ellipsis;
|
||||||
|
tmp.richText = true;
|
||||||
|
|
||||||
|
var textRect = textObj.GetComponent<RectTransform>();
|
||||||
|
textRect.anchorMin = Vector2.zero;
|
||||||
|
textRect.anchorMax = Vector2.one;
|
||||||
|
textRect.offsetMin = new Vector2(8, 2);
|
||||||
|
textRect.offsetMax = new Vector2(-4, -2);
|
||||||
|
|
||||||
|
_entries.Add(entry);
|
||||||
|
|
||||||
|
while (_entries.Count > 100)
|
||||||
|
{
|
||||||
|
Destroy(_entries[0]);
|
||||||
|
_entries.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_visible)
|
||||||
|
{
|
||||||
|
_unread++;
|
||||||
|
UpdateToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
||||||
|
Canvas.ForceUpdateCanvases();
|
||||||
|
_scrollRect.verticalNormalizedPosition = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Clear()
|
||||||
|
{
|
||||||
|
foreach (var e in _entries) Destroy(e);
|
||||||
|
_entries.Clear();
|
||||||
|
_unread = 0;
|
||||||
|
UpdateToggle();
|
||||||
|
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
||||||
|
AddLog("已清除", Color.yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Toggle()
|
||||||
|
{
|
||||||
|
_visible = !_visible;
|
||||||
|
_panel.SetActive(_visible);
|
||||||
|
if (_visible) _unread = 0;
|
||||||
|
UpdateToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateToggle()
|
||||||
|
{
|
||||||
|
if (_toggleText != null)
|
||||||
|
_toggleText.text = _visible ? "隐藏日志" : (_unread > 0 ? $"日志({_unread})" : "显示日志");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
unity-client/Assets/Scripts/EventLog.cs.meta
Normal file
2
unity-client/Assets/Scripts/EventLog.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 29d056ffaee4442e2b947b862a50d329
|
||||||
8
unity-client/Assets/Scripts/UI.meta
Normal file
8
unity-client/Assets/Scripts/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3a714afb8c6c4492e838e7fcc1e6de1b
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -28,6 +28,7 @@ namespace TheIsland.UI
|
|||||||
private Button _resetButton;
|
private Button _resetButton;
|
||||||
private GameObject _notificationPanel;
|
private GameObject _notificationPanel;
|
||||||
private TextMeshProUGUI _notificationText;
|
private TextMeshProUGUI _notificationText;
|
||||||
|
// EventLog 现在自己管理初始化
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region State
|
#region State
|
||||||
@@ -149,6 +150,7 @@ namespace TheIsland.UI
|
|||||||
CreateTopBar();
|
CreateTopBar();
|
||||||
CreateBottomBar();
|
CreateBottomBar();
|
||||||
CreateNotificationPanel();
|
CreateNotificationPanel();
|
||||||
|
// EventLog 自己管理初始化,不在这里创建
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateTopBar()
|
private void CreateTopBar()
|
||||||
|
|||||||
@@ -191,8 +191,10 @@ namespace TheIsland.Visual
|
|||||||
|
|
||||||
var velocityOverLifetime = _fogSystem.velocityOverLifetime;
|
var velocityOverLifetime = _fogSystem.velocityOverLifetime;
|
||||||
velocityOverLifetime.enabled = true;
|
velocityOverLifetime.enabled = true;
|
||||||
velocityOverLifetime.x = new ParticleSystem.MinMaxCurve(-0.2f, 0.2f);
|
// 所有轴使用相同的曲线模式 (Constant)
|
||||||
velocityOverLifetime.y = new ParticleSystem.MinMaxCurve(0.05f, 0.1f);
|
velocityOverLifetime.x = 0f;
|
||||||
|
velocityOverLifetime.y = 0.08f;
|
||||||
|
velocityOverLifetime.z = 0f;
|
||||||
|
|
||||||
var colorOverLifetime = _fogSystem.colorOverLifetime;
|
var colorOverLifetime = _fogSystem.colorOverLifetime;
|
||||||
colorOverLifetime.enabled = true;
|
colorOverLifetime.enabled = true;
|
||||||
@@ -234,8 +236,10 @@ namespace TheIsland.Visual
|
|||||||
|
|
||||||
var velocityOverLifetime = _heatSystem.velocityOverLifetime;
|
var velocityOverLifetime = _heatSystem.velocityOverLifetime;
|
||||||
velocityOverLifetime.enabled = true;
|
velocityOverLifetime.enabled = true;
|
||||||
|
// 所有轴使用相同的曲线模式 (Constant)
|
||||||
|
velocityOverLifetime.x = 0f;
|
||||||
velocityOverLifetime.y = 1f;
|
velocityOverLifetime.y = 1f;
|
||||||
velocityOverLifetime.x = new ParticleSystem.MinMaxCurve(-0.3f, 0.3f);
|
velocityOverLifetime.z = 0f;
|
||||||
|
|
||||||
var colorOverLifetime = _heatSystem.colorOverLifetime;
|
var colorOverLifetime = _heatSystem.colorOverLifetime;
|
||||||
colorOverLifetime.enabled = true;
|
colorOverLifetime.enabled = true;
|
||||||
@@ -277,7 +281,10 @@ namespace TheIsland.Visual
|
|||||||
|
|
||||||
var velocityOverLifetime = _cloudSystem.velocityOverLifetime;
|
var velocityOverLifetime = _cloudSystem.velocityOverLifetime;
|
||||||
velocityOverLifetime.enabled = true;
|
velocityOverLifetime.enabled = true;
|
||||||
|
// 所有轴使用相同的曲线模式 (Constant)
|
||||||
velocityOverLifetime.x = 0.3f;
|
velocityOverLifetime.x = 0.3f;
|
||||||
|
velocityOverLifetime.y = 0f;
|
||||||
|
velocityOverLifetime.z = 0f;
|
||||||
|
|
||||||
var renderer = cloudObj.GetComponent<ParticleSystemRenderer>();
|
var renderer = cloudObj.GetComponent<ParticleSystemRenderer>();
|
||||||
renderer.material = CreateCloudMaterial();
|
renderer.material = CreateCloudMaterial();
|
||||||
|
|||||||
397
unity-client/Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset
Executable file → Normal file
397
unity-client/Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset
Executable file → Normal file
File diff suppressed because one or more lines are too long
@@ -15,7 +15,7 @@ MonoBehaviour:
|
|||||||
assetVersion: 2
|
assetVersion: 2
|
||||||
m_TextWrappingMode: 1
|
m_TextWrappingMode: 1
|
||||||
m_enableKerning: 1
|
m_enableKerning: 1
|
||||||
m_ActiveFontFeatures: 00000000
|
m_ActiveFontFeatures: 6e72656b
|
||||||
m_enableExtraPadding: 0
|
m_enableExtraPadding: 0
|
||||||
m_enableTintAllSprites: 0
|
m_enableTintAllSprites: 0
|
||||||
m_enableParseEscapeCharacters: 1
|
m_enableParseEscapeCharacters: 1
|
||||||
@@ -36,17 +36,14 @@ MonoBehaviour:
|
|||||||
m_fallbackFontAssets: []
|
m_fallbackFontAssets: []
|
||||||
m_matchMaterialPreset: 1
|
m_matchMaterialPreset: 1
|
||||||
m_HideSubTextObjects: 0
|
m_HideSubTextObjects: 0
|
||||||
m_defaultSpriteAsset: {fileID: 11400000, guid: c41005c129ba4d66911b75229fd70b45,
|
m_defaultSpriteAsset: {fileID: 11400000, guid: c41005c129ba4d66911b75229fd70b45, type: 2}
|
||||||
type: 2}
|
|
||||||
m_defaultSpriteAssetPath: Sprite Assets/
|
m_defaultSpriteAssetPath: Sprite Assets/
|
||||||
m_enableEmojiSupport: 1
|
m_enableEmojiSupport: 1
|
||||||
m_MissingCharacterSpriteUnicode: 0
|
m_MissingCharacterSpriteUnicode: 0
|
||||||
m_EmojiFallbackTextAssets: []
|
m_EmojiFallbackTextAssets: []
|
||||||
m_defaultColorGradientPresetsPath: Color Gradient Presets/
|
m_defaultColorGradientPresetsPath: Color Gradient Presets/
|
||||||
m_defaultStyleSheet: {fileID: 11400000, guid: f952c082cb03451daed3ee968ac6c63e,
|
m_defaultStyleSheet: {fileID: 11400000, guid: f952c082cb03451daed3ee968ac6c63e, type: 2}
|
||||||
type: 2}
|
|
||||||
m_StyleSheetsResourcePath:
|
m_StyleSheetsResourcePath:
|
||||||
m_leadingCharacters: {fileID: 4900000, guid: d82c1b31c7e74239bff1220585707d2b, type: 3}
|
m_leadingCharacters: {fileID: 4900000, guid: d82c1b31c7e74239bff1220585707d2b, type: 3}
|
||||||
m_followingCharacters: {fileID: 4900000, guid: fade42e8bc714b018fac513c043d323b,
|
m_followingCharacters: {fileID: 4900000, guid: fade42e8bc714b018fac513c043d323b, type: 3}
|
||||||
type: 3}
|
|
||||||
m_UseModernHangulLineBreakingRules: 0
|
m_UseModernHangulLineBreakingRules: 0
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ PlayerSettings:
|
|||||||
androidSupportedAspectRatio: 1
|
androidSupportedAspectRatio: 1
|
||||||
androidMaxAspectRatio: 2.4
|
androidMaxAspectRatio: 2.4
|
||||||
androidMinAspectRatio: 1
|
androidMinAspectRatio: 1
|
||||||
applicationIdentifier: {}
|
applicationIdentifier:
|
||||||
|
Standalone: com.DefaultCompany.unity-client
|
||||||
buildNumber:
|
buildNumber:
|
||||||
Standalone: 0
|
Standalone: 0
|
||||||
VisionOS: 0
|
VisionOS: 0
|
||||||
|
|||||||
Reference in New Issue
Block a user