QQ20241225-163158.1.mp4
成果展示
QQ202522-224352.1.mp4
鱼群模拟
遇到最大的问题就是Git拉取到一半报错: early EOF Fetch-pack: invalid index-pack output,拼尽全力无法战胜,只要depth大于1就拉取不了,最后也不了了之(只拉取depth=1的部分也能用不是吗。)
这部分倒是没什么问题,编译了我九个小时,从半夜十二点半编译到早上十点,听知乎上说incredibuild很好用,折腾了一下发现需要在分布式环境内用,遂不了了之
(有几个库文件没引用好导致编译失败两次,当我拼尽两天终于看到这个logo的时候如释重负)
配置的时候发现插件要在epic商城对应的版本下,下载完了又发现
这个VS Integration Tool不支持我的5.5,心碎。
不过好在问题不大。
遇到两个大问题:
第一个是Android Studio安装好SDK之后不知道为什么,不停弹出
点了Yes还会继续弹,我看网上这一步已经自动帮你配好环境变量了,但是我的就是进行不下去,所以识别不出我的SDK
解决方案就是,我重新设置了一下它的SDK路径,
能跑就行吧。
然后就是打包的时候疯狂给我爆这个错
发现是下载gardle的时候因为网络环境出错,于是自己在网上下载一版放到了gardle文件夹里
然后终于打包完成
尝试了一下用蓝图+OnlineSession做一个局域网联机
直连、会话搜索 没什么好说的,直接用的OnlineSession的接口
房间达到两个人就会开始倒计时,倒计时结束显示计分板
基本的网络同步、用PlayerState维护的分数,用GameInstance跨场景保存的名字,GameMode进行的流程控制,UMG计分板
以及作业要求的计分系统,一开始变小的方块就是重要方块
(图片左侧实际上是我用枪的模型代替了人物模型,折腾一晚上懒得导入其它模型了)
在服务端生成后同步到客户端
FindSessions是一个异步方法,所以在它后面同步输出Results的个数,会是0
这样输出就没问题
这里出了很多问题,主要原因还是在一开始没搞懂他的同步机制和跨场景机制
总结:同步方面应当尽量把客户端逻辑写在Controller的Event里,然后服务端广播,需要跨场景的重要信息可以使用GameInstance或者Subsystem进行存储(类似单例),也可以使用无缝加载
考试+乱七八糟的东西,鸽了()
之前已经实现了基本的会话连接,此处直接用GameInstance维护一个本地的账号验证map就可以(服务器验证也是一个道理,图省事直接做成本地的了)
先用UMG搓一个UI
因为场景加载是离散的,为了好看一点就做个假进度条
因为一直在按deltaTime插值,所以最后会越来越慢,只需要加载完成的时候填满就好了。
UE没提供OpenLevel的Async版本,要用流式加载,不过因为我这项目加载几乎是瞬间,就直接做个假进度条凑合凑合舒服一点就行
客户端使用JoinSession,同样是连接处理完毕后瞬间加入(加入后会自动销毁UI),这里直接同步创建一个进度条
因为涉及击中目标准星要发生变化,所以生成子弹球就不太合适了
改为射线检测(在服务端)
广播到客户端以显示弹道
依旧是先创建UI
生命值和子弹数依旧记录在PlayerState里
生命值很好处理,只有两个玩家,似了一个直接调用结束
因为之前UE给的模板直接把Fire跟输入绑定了,所以懒得改代码直接给Fire加一个判断就好
又因为子弹数是写在蓝图里的,所以我们要开放给蓝图
设定为虚函数,直接在蓝图类里继承写个判断
效果:
没什么好说的,打到的时候通知给PlayerController再通知给UI
上周已经做过了,这周微调了一下,加了个Died状态
稍微研究了一下行为树、黑板、AI感知
新建了个敌人Character,实现接口
Controller负责执行行为树和处理感知
(期末好多事就鸽了,不过发现中间那几周的作业我基本之前就已经做完了)
不限引擎,所以用我最熟悉的Unity来做吧,再熟悉熟悉DOTS和SOA编程
1.一个鱼群要有一个虚拟领袖:需要有一个组件或是bool值标记一条鱼是否是领袖。
2.鱼和鱼之间要有间距,鱼群和鱼群之间也要有间距:计算时要留出这些可调节的间距
3.最多鱼数(这个我想多调一点,测试以下DOTS性能)
4.性能优化:使用IJobEntity并行处理鱼的移动。性能数据:使用Unity自带的Stats显示和Profiler就可以看到Job的分配情况和主要性能瓶颈。
public struct FishData : IComponentData
{
public int GroupId; // 鱼群编号
public float2 Velocity; // 当前速度(2D向量)
public bool IsLeader; // 是否为虚拟领袖
}Wiki上对Boid规则三要素的阐述:
Separation:若距离过近,则个体会尝试远离。
Alignment:若个体离群体过远,则尝试靠近中心。
Cohesion:个体会尝试匹配邻居的速度和方向。
作用:查询所有旧帧数据并将其分配给Job、调度Job
/// <summary>
/// 因为我们需要收集旧帧数据作为参考,用一个新的数据结构存储
/// </summary>
public struct FishInfo
{
public int GroupId;
public float2 Position;
public float2 Velocity;
public bool IsLeader;
}
[BurstCompile]
public partial struct FlockingSystem : ISystem
{
private EntityQuery query;
public void OnCreate(ref SystemState state)
{
//初始化query,用来计算鱼的数量,方便我们alloc
query = state.GetEntityQuery(ComponentType.ReadOnly<FishData>(), ComponentType.ReadOnly<LocalTransform>());
}
public void OnUpdate(ref SystemState state)
{
int fishCount = query.CalculateEntityCount();
// 为后续计算分配全局数据数组与每个Entity的index映射的HashMap
// 为什么要用数组+索引映射?因为之后要涉及大量连续访问,HashMap缓存效率太低
NativeArray<FishInfo> fishInfos = new NativeArray<FishInfo>(fishCount, Allocator.TempJob);
NativeParallelHashMap<Entity, int> entityIndices = new NativeParallelHashMap<Entity, int>(fishCount, Allocator.TempJob);
int index = 0;
foreach (var ( fish, translation,entity) in SystemAPI.Query< FishData, LocalTransform>().WithEntityAccess())
{
entityIndices.TryAdd(entity, index);
fishInfos[index] = new FishInfo
{
GroupId = fish.GroupId,
Position = new float2(translation.Position.x, translation.Position.y),
Velocity = fish.Velocity,
IsLeader = fish.IsLeader
};
index++;
}
// 运动参数,这里hardcode一下
float deltaTime = SystemAPI.Time.DeltaTime;
float neighborRadius = 20.0f; //能感知到多远的邻居
float separationDistance = 5f; //安全距离,防止碰撞
float alignmentWeight = 1.0f; //对齐行为的权重
float cohesionWeight = 1.0f; //凝聚行为的权重
float separationWeight = 1.5f;//分离行为的权重
float leaderFollowWeight = 2.0f;//跟随领袖的权重
float maxSpeed = 5.0f;//最大移速
// 构造 IJobEntity 任务,并将全局数据传入
var job = new FlockingJobEntity
{
FishInfos = fishInfos,
EntityIndices = entityIndices,
DeltaTime = deltaTime,
NeighborRadius = neighborRadius,
SeparationDistance = separationDistance,
AlignmentWeight = alignmentWeight,
CohesionWeight = cohesionWeight,
SeparationWeight = separationWeight,
LeaderFollowWeight = leaderFollowWeight,
MaxSpeed = maxSpeed
};
// 调度 Job(使用ScheduleParallel并行更新所有的鱼)
state.Dependency = job.ScheduleParallel(state.Dependency);
//Job结束的时候销毁entityIndices(NativeParallelHashMap无法使用[DeallocateOnJobCompletion]自动销毁)
state.Dependency = entityIndices.Dispose(state.Dependency);
}
}作用:对每条鱼分别执行逻辑判断和移动
[BurstCompile]
public partial struct FlockingJobEntity : IJobEntity
{
/// <summary>
/// 全局鱼数据数组
/// </summary>
[ReadOnly]
[DeallocateOnJobCompletion] //Job结束后销毁
public NativeArray<FishInfo> FishInfos;
/// <summary>
/// 从 Entity 到全局数据数组索引的映射
/// </summary>
[ReadOnly]
public NativeParallelHashMap<Entity, int> EntityIndices;
//所有运动参数
public float DeltaTime;
public float NeighborRadius;
public float SeparationDistance;
public float AlignmentWeight;
public float CohesionWeight;
public float SeparationWeight;
public float LeaderFollowWeight;
public float MaxSpeed;
void Execute(Entity entity, ref FishData fish, ref LocalTransform translation)
{
// 找到自己的数据
int index = EntityIndices[entity];
FishInfo currentFish = FishInfos[index];
// 旋转速度,直接hardcode
float rotationSpeed = 10f;
// 如果该鱼为领袖,单独计算,简单更新位置
if (currentFish.IsLeader)
{
float2 newPos = currentFish.Position + currentFish.Velocity * DeltaTime;
translation.Position = new float3(newPos.x, newPos.y, translation.Position.z);
// 若速度足够大,则更新旋转
if (math.length(currentFish.Velocity) > 0.001f)
{
// 插值旋转使它更平滑
quaternion targetRotation = quaternion.RotateZ(math.atan2(currentFish.Velocity.y, currentFish.Velocity.x));
translation.Rotation = math.slerp(translation.Rotation, targetRotation, DeltaTime * rotationSpeed);
}
return;
}
// 非领袖鱼,计算力:分离、对齐、聚合
float2 separation = float2.zero;
float2 alignment = float2.zero;
float2 cohesion = float2.zero;
//自己同群的数目
int neighborCount = 0;
// 遍历所有鱼数据,计算同群邻居影响
for (int i = 0; i < FishInfos.Length; i++)
{
if (i == index)
continue;
FishInfo other = FishInfos[i];
float dist = math.distance(currentFish.Position, other.Position);
// 对于同群鱼
if (other.GroupId == currentFish.GroupId)
{
if (dist < NeighborRadius)
{
alignment += other.Velocity;
cohesion += other.Position;
neighborCount++;
} //凝聚和对齐
if (dist < SeparationDistance)
{
separation += (currentFish.Position - other.Position) / (dist + 0.0001f);
} //分离
}
// 对于不同群鱼:采用较大范围内的分离,效果明显
else
{
if (dist < SeparationDistance * 2.0f)
{
separation += (currentFish.Position - other.Position) / (dist + 0.0001f);
}
}
}
if (neighborCount > 0)
{
alignment /= neighborCount;
cohesion /= neighborCount;
// 聚合力:指向群体中心
cohesion = cohesion - currentFish.Position;
}
// 寻找该鱼群的虚拟领袖(取第一个找到的领袖)
float2 leaderForce = float2.zero;
bool leaderFound = false;
for (int i = 0; i < FishInfos.Length; i++)
{
FishInfo other = FishInfos[i];
if (other.GroupId == currentFish.GroupId && other.IsLeader)
{
leaderForce = other.Velocity;
leaderFound = true;
break;
}
}
if (!leaderFound)
{
leaderForce = float2.zero;
}
// 综合各个行为力
float2 acceleration = alignment * AlignmentWeight +
cohesion * CohesionWeight +
separation * SeparationWeight +
leaderForce * LeaderFollowWeight;
float2 newVelocity = currentFish.Velocity + acceleration * DeltaTime;
float speed = math.length(newVelocity);
if (speed > MaxSpeed)
{
newVelocity = newVelocity / speed * MaxSpeed;
}
float2 newPosition = currentFish.Position + newVelocity * DeltaTime;
// 更新鱼的速度和位置
fish.Velocity = newVelocity;
translation.Position = new float3(newPosition.x, newPosition.y, translation.Position.z);
// 平滑旋转
if (math.length(newVelocity) > 0.001f)
{
quaternion targetRotation = quaternion.RotateZ(math.atan2(newVelocity.y, newVelocity.x));
translation.Rotation = math.slerp(translation.Rotation, targetRotation, DeltaTime * rotationSpeed);
}
}
}上千只鱼也可以稳定在较高帧率
可以看到Job被分配到多个线程
cpu高峰出现在Render上,由于我渲染层采用的是SpriteRender,在SRP Batcher的加持下可以看到Batch达到了三千多,Drawcall达到了二百多。优化的话可能要用到Entities Graphics Batcher,不过没了解太深,就这样吧。














































