(client) feat:实现技能树界面,实现地图生成器,实现维度指定,实现规则瓦片定义,实现逃跑逻辑,实现消息定义,实现武器动画,实现受击动画 fix: 修复单攻击子弹击中多个目标,修复人物属性计算错误 (#56)

Co-authored-by: m0_75251201 <m0_75251201@noreply.gitcode.com>
Reviewed-on: http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite/pulls/56
This commit is contained in:
2025-09-19 08:26:54 +08:00
parent 78849e0cc5
commit 87a8abe86c
282 changed files with 19364 additions and 8824 deletions

View File

@@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using Data;
using Prefab;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Utils
{
public class GameObjectCreate:Singleton<GameObjectCreate>
{
private SpriteAnimator _animatorPrefab;
public SpriteAnimator AnimatorPrefab
{
get
{
if (_animatorPrefab == null)
{
_animatorPrefab=Resources.Load<SpriteAnimator>("Prefab/Animation");
}
return _animatorPrefab;
}
}
private ImagePrefab _imagePrefab;
public ImagePrefab ImagePrefab
{
get
{
if (_imagePrefab == null)
{
_imagePrefab=Resources.Load<ImagePrefab>("Prefab/Image");
}
return _imagePrefab;
}
}
/// <summary>
/// 递归初始化单个绘图节点及其子节点,具有更强的健壮性和错误处理。
/// </summary>
/// <param name="drawNode">绘图节点定义。</param>
/// <param name="parent">父节点对象。</param>
/// <returns>创建的GameObject如果失败则返回null</returns>
public static GameObject InitBodyPart(DrawNodeDef drawNode, GameObject parent)
{
try
{
// 参数验证
if (drawNode == null)
{
Debug.LogWarning("InitBodyPart: drawNode参数为null");
return null;
}
if (!parent)
{
Debug.LogWarning($"InitBodyPart: 父节点为null (节点名: {drawNode.nodeName})");
return null;
}
GameObject nodeObject = null;
// 根据纹理数量创建不同类型的节点
switch (drawNode.textures?.Length ?? 0)
{
case 0:
// 无纹理节点
nodeObject = new GameObject(drawNode.nodeName);
nodeObject.transform.SetParent(parent.transform, false);
break;
case 1:
// 单纹理节点
if (!Instance.ImagePrefab)
{
Debug.LogError($"InitBodyPart: imagePrefab未设置 (节点名: {drawNode.nodeName})");
return null;
}
if (drawNode.textures != null)
{
var texture =
Managers.PackagesImageManager.Instance.GetSprite(drawNode.textures[0]);
if (!texture)
{
Debug.LogWarning(
$"InitBodyPart: 无法获取纹理 (节点名: {drawNode.nodeName}, 纹理ID: {drawNode.textures[0]})");
}
var imagePrefabCom = Object.Instantiate(Instance.ImagePrefab, parent.transform);
if (imagePrefabCom)
{
imagePrefabCom.SetSprite(texture);
nodeObject = imagePrefabCom.gameObject;
}
else
{
Debug.LogWarning($"InitBodyPart: 无法获取ImagePrefab组件 (节点名: {drawNode.nodeName})");
}
}
break;
default:
// 多纹理动画节点
if (!Instance.AnimatorPrefab)
{
Debug.LogError($"InitBodyPart: animatorPrefab未设置 (节点名: {drawNode.nodeName})");
return null;
}
var animator = Object.Instantiate(Instance.AnimatorPrefab, parent.transform);
if (!animator)
{
Debug.LogWarning($"InitBodyPart: 无法获取SpriteAnimator组件 (节点名: {drawNode.nodeName})");
break;
}
animator.SetFPS(drawNode.FPS);
if (drawNode.textures != null)
{
var animatedSprites =
Managers.PackagesImageManager.Instance.GetSprites(drawNode.textures);
if (animatedSprites.Length > 0)
{
animator.SetSprites(animatedSprites);
nodeObject = animator.gameObject;
}
else
{
Debug.LogWarning($"InitBodyPart: 没有有效的动画纹理 (节点名: {drawNode.nodeName})");
}
}
break;
}
// 设置节点属性
if (!nodeObject) return nodeObject;
nodeObject.transform.localPosition = drawNode.position;
nodeObject.name = drawNode.nodeName ?? "UnnamedNode";
// 递归初始化子节点
if (drawNode.nodes == null) return nodeObject;
foreach (var child in drawNode.nodes)
{
try
{
InitBodyPart(child, nodeObject);
}
catch (Exception ex)
{
Debug.LogError($"InitBodyPart: 初始化子节点失败 (父节点: {drawNode.nodeName}): {ex.Message}");
}
}
return nodeObject;
}
catch (Exception ex)
{
Debug.LogError($"InitBodyPart: 初始化节点时发生未处理的异常 (节点名: {drawNode?.nodeName}): {ex}");
return null;
}
}
public static SpriteAnimator SpriteAnimator(Sprite[] sprites,Transform parent)
{
var spriteAnimator = Object.Instantiate(Instance.AnimatorPrefab,parent);
spriteAnimator.SetSprites(sprites);
spriteAnimator.transform.localPosition=Vector3.zero;
return spriteAnimator;
}
public static SpriteAnimator SpriteAnimator(string[] spriteDefNames, Transform parent)
{
return SpriteAnimator(Managers.PackagesImageManager.Instance.GetSprites(spriteDefNames), parent);
}
public static ImagePrefab SpriteImage(Sprite sprite, Transform parent)
{
var spriteImage = Object.Instantiate(Instance.ImagePrefab,parent);
spriteImage.SetSprite(sprite);
spriteImage.transform.localPosition=Vector3.zero;
return spriteImage;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 743325e6a39b411981605dd184286c93
timeCreated: 1757077655

View File

@@ -11,7 +11,10 @@ namespace Utils
Initialize();
}
// 初始化混淆表
/// <summary>
/// 初始化混淆表。
/// </summary>
/// <param name="seed">随机种子。</param>
private void Initialize(int seed = 0)
{
var permutation = new int[256];
@@ -36,28 +39,48 @@ namespace Utils
}
}
// 重新指定种子
/// <summary>
/// 重新指定种子并初始化混淆表。
/// </summary>
/// <param name="seed">随机种子。</param>
public void Reinitialize(int seed)
{
Initialize(seed);
}
// 平滑函数 (6t^5 - 15t^4 + 10t^3)
/// <summary>
/// Perlin噪声平滑函数 (6t^5 - 15t^4 + 10t^3)。
/// </summary>
/// <param name="t">输入值。</param>
/// <returns>平滑后的值。</returns>
private static double Fade(double t)
{
return t * t * t * (t * (t * 6 - 15) + 10);
}
// 线性插值
/// <summary>
/// 线性插值。
/// </summary>
/// <param name="t">插值因子 (0-1)。</param>
/// <param name="a">起始值。</param>
/// <param name="b">结束值。</param>
/// <returns>插值结果。</returns>
private static double Lerp(double t, double a, double b)
{
return a + t * (b - a);
}
// 计算梯度向量和距离向量的点积
/// <summary>
/// 计算梯度向量和距离向量的点积。
/// </summary>
/// <param name="hash">哈希值,用于决定梯度向量方向。</param>
/// <param name="x">X轴距离。</param>
/// <param name="y">Y轴距离。</param>
/// <param name="z">Z轴距离。</param>
/// <returns>点积结果。</returns>
private static double Grad(int hash, double x, double y, double z)
{
return (hash & 0xF) switch // 取hash值的最后4位
return (hash & 0xF) switch // 取哈希值的最后4位决定梯度方向
{
0x0 => x + y,
0x1 => -x + y,
@@ -80,9 +103,13 @@ namespace Utils
}
/// <summary>
/// 为给定的(x, y, z)坐标生成3D Perlin噪声。
/// 输出值通常在-1到1之间。
/// 为给定的(x, y, z)坐标生成3D Perlin噪声。
/// 输出值通常在-1到1之间。
/// </summary>
/// <param name="x">X坐标。</param>
/// <param name="y">Y坐标默认为0。</param>
/// <param name="z">Z坐标默认为0。</param>
/// <returns>Perlin噪声值。</returns>
public double Noise(double x, double y = 0, double z = 0)
{
var X = (int)Math.Floor(x) & 255;
@@ -126,4 +153,4 @@ namespace Utils
return Lerp(w, y0, y1);
}
}
}
}

View File

@@ -0,0 +1,21 @@
using UnityEngine;
namespace Utils
{
public static class RotateTool
{
// 旋转对象到指定方向
public static void RotateTransformToDirection(Transform transform, Vector3 targetDirection)
{
// 确保目标方向不是零向量
if (targetDirection == Vector3.zero)
return;
// 计算当前向上方向与目标方向之间的角度
var angle = Mathf.Atan2(targetDirection.y, targetDirection.x) * Mathf.Rad2Deg;
// 应用旋转
transform.rotation = Quaternion.Euler(0f, 0f, angle);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d25c353b3bed400f92bccc0986b22420
timeCreated: 1757241772

View File

@@ -0,0 +1,213 @@
// 文件: Assets/Scripts/Utils/StringUtils.cs (或你的实际路径)
using System;
using System.Reflection;
using Map;
namespace Utils
{
using UnityEngine; // 引入Unity命名空间以便使用Vector2和Vector3
public static class StringUtils
{
/// <summary>
/// 尝试从字符串中解析出指定数量的浮点数组件。
/// 这是StringToVector2和StringToVector3的核心辅助方法。
/// </summary>
/// <param name="source">要解析的源字符串。</param>
/// <param name="expectedComponentCount">期望的组件数量例如Vector2为2Vector3为3。</param>
/// <param name="parsedComponents">如果解析成功,包含解析出的浮点数;否则为 null。</param>
/// <param name="defaultComponentValue">在解析失败时用于填充parsedComponents数组的默认值。</param>
/// <returns>如果所有组件都成功解析,则返回 true否则返回 false。</returns>
private static bool TryParseVectorComponents(
string source,
int expectedComponentCount,
out float[] parsedComponents,
float defaultComponentValue = 0f)
{
parsedComponents = new float[expectedComponentCount];
if (string.IsNullOrWhiteSpace(source))
{
// 如果字符串为空或空白,则用默认值填充并失败
for (int i = 0; i < expectedComponentCount; i++)
{
parsedComponents[i] = defaultComponentValue;
}
return false;
}
// 移除所有可能存在的括号或方括号
// 采用Replace而不是Regex是因为简单字符替换的性能优势
string cleanedSource = source
.Replace("[", "")
.Replace("]", "")
.Replace("(", "")
.Replace(")", "");
// 定义所有可能的分隔符:逗号、空格、分号
// 使用 StringSplitOptions.RemoveEmptyEntries 确保即使有多个分隔符连续出现,也不会创建空字符串项
char[] delimiters = { ',', ' ', ';' };
string[] componentStrings = cleanedSource.Split(
delimiters,
System.StringSplitOptions.RemoveEmptyEntries);
if (componentStrings.Length < expectedComponentCount)
{
// 如果组件数量不足,则用默认值填充并失败
for (int i = 0; i < expectedComponentCount; i++)
{
parsedComponents[i] = defaultComponentValue;
}
return false;
}
for (int i = 0; i < expectedComponentCount; i++)
{
// 尝试解析每个组件,并去除前后空格
if (!float.TryParse(componentStrings[i].Trim(), out parsedComponents[i]))
{
// 如果任何一个组件解析失败,则用默认值填充所有组件并返回 false
for (int j = 0; j < expectedComponentCount; j++)
{
parsedComponents[j] = defaultComponentValue;
}
return false;
}
}
return true; // 所有组件都成功解析
}
/// <summary>
/// 将字符串转换为 Unity 的 Vector2。
/// 支持格式如 "1,2", "[1 2]", "(1;2)",并能处理空格。
/// </summary>
/// <param name="str">要转换的字符串。</param>
/// <param name="defaultValue">转换失败时返回的默认 Vector2 值。</param>
/// <returns>转换后的 Vector2 值或默认值。</returns>
public static Vector2 StringToVector2(string str, Vector2 defaultValue = default)
{
if (defaultValue == default)
{
defaultValue = Vector2.zero; // 如果未指定,则使用 Vector2.zero 作为默认值
}
// 默认浮点值为0f用于内部解析失败填充
if (TryParseVectorComponents(str, 2, out var components))
{
return new Vector2(components[0], components[1]);
}
return defaultValue;
}
/// <summary>
/// 将字符串转换为 Unity 的 Vector3。
/// 支持格式如 "1,2,3", "[1 2 3]", "(1;2;3)",并能处理空格。
/// </summary>
/// <param name="str">要转换的字符串。</param>
/// <param name="defaultValue">转换失败时返回的默认 Vector3 值。</param>
/// <returns>转换后的 Vector3 值或默认值。</returns>
public static Vector3 StringToVector3(string str, Vector3 defaultValue = default)
{
if (defaultValue == default)
{
defaultValue = Vector3.zero; // 如果未指定,则使用 Vector3.zero 作为默认值
}
// 默认浮点值为0f用于内部解析失败填充
if (TryParseVectorComponents(str, 3, out var components))
{
return new Vector3(components[0], components[1], components[2]);
}
return defaultValue;
}
/// <summary>
/// 根据类名字符串创建并返回一个继承自 MapGeneratorWorkClassBase 的实例。
/// </summary>
/// <param name="className">不含命名空间的类名。</param>
/// <returns>对应的 MapGeneratorWorkClassBase 实例,如果找不到或类型不匹配则返回 null。</returns>
public static MapGeneratorWorkClassBase CreateMapGeneratorInstance(string className)
{
// 定义优先查找的命名空间
const string preferredNamespace = "Map";
Type foundType = null;
// 第一遍:优先在指定的命名空间下查找
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
string fullClassNameInPreferredNamespace = preferredNamespace + "." + className;
Type type = assembly.GetType(fullClassNameInPreferredNamespace);
if (type != null)
{
foundType = type;
break; // 找到后立即退出循环,因为我们找到了优先的类型
}
}
// 如果在优先命名空间中没有找到则进行第二遍在所有命名空间中查找不指定命名空间即只用className
if (foundType == null)
{
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type type = assembly.GetType(className);
if (type != null)
{
// 如果找到多个同名的类型在不同命名空间但都不在preferredNamespace
// 这里的处理是取第一个找到的。如果需要更复杂的选择逻辑,例如按顺序优先级,
// 则需要收集所有可能类型并进行筛选。
// 但对于通常的应用如果className本身是不带命名空间的这通常意味着它在全局作用域
// 或者我们期望它在一个特定的命名空间如preferredNamespace中。
foundType = type;
// 注意这里没有break;的话如果不同程序集有同名类会继续查找并覆盖foundType。
// 如果你期望找到第一个就返回可以加上break;
// 如果你期望找到一个满足条件的就返回,那么把后续的检查放到这里。
// 为了简化我们只在这里找到一个可用的Type然后统一在后面检查。
break; // 找到了一个,先用这个。
}
}
}
// 对找到的类型进行有效性检查和实例创建
if (foundType != null)
{
// 检查类型是否:
// 1. 是 MapGeneratorWorkClassBase 的子类或本身就是 MapGeneratorWorkClassBase
// 2. 不是抽象类
// 3. 有一个无参构造函数
if (typeof(MapGeneratorWorkClassBase).IsAssignableFrom(foundType) &&
!foundType.IsAbstract &&
foundType.GetConstructor(Type.EmptyTypes) != null) // 检查是否有无参构造函数
{
try
{
// 使用 Activator.CreateInstance 创建实例
return (MapGeneratorWorkClassBase)Activator.CreateInstance(foundType);
}
catch (Exception ex)
{
Debug.LogError($"Error creating instance of {foundType.FullName}: {ex.Message}");
return null;
}
}
string foundTypeName = foundType.FullName;
Debug.LogError($"Type '{foundTypeName}' found but does not meet criteria:" +
$" IsAssignableFrom({nameof(MapGeneratorWorkClassBase)}): {typeof(MapGeneratorWorkClassBase).IsAssignableFrom(foundType)}," +
$" IsAbstract: {foundType.IsAbstract}," +
$" HasParameterlessConstructor: {foundType.GetConstructor(Type.EmptyTypes) != null}");
return null; // 类型不满足条件
}
Debug.LogError(
$"Type '{className}' not found in preferred namespace '{preferredNamespace}' or any loaded assembly.");
return null; // 类型未找到
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4a48d4a90a784e4586f2faceba76f3b1
timeCreated: 1757846637