写在前面 有个室友上个学期选了一堆课,导致这个学期无课可上,整天处于无聊的状态中,嘟囔着说要找点事干。寝室展开了讨论,决定做一款游戏出来,于是我开始学习unity为之后的游戏开发练手。恰好几天前刷b站看到了一个搬运视频”类鸟群系统”的实现,就决定将其作为第一个练手项目了:)。
这里挂出b站视频地址带你实现一个类鸟群系统_哔哩哔哩_bilibili
以及算法文章出处Craig Reynolds: Flocks, Herds, and Schools: A Distributed Behavioral Model (toronto.edu)
代码结构 代码由一个种群行为模拟器和一个粒子生成器构成
种群行为计算和粒子渲染分开进行具有更高效率
个体定义 1 2 3 4 5 6 7 public struct Fish{ public Vector2 position; public Vector2 velocity; public float wanderAngle; public int type; }
种群行为模拟 核心公式
v += f*t
p += v*t
注意这个f代表的**”力”**,在后面将会详细说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 private void Start (){ summonFlock(); for (int i = 2 ; i <= init_num / 2 - 1 ; i++) bornFish(2 ); for (int i = 2 ; i <= init_num / 2 - 1 ; i++) bornFish(1 ); for (int i = 0 ; i <= shark_num - 1 ; i++) bornFish(0 ); } void FixedUpdate (){ float deltaTime = Time.deltaTime; for (int i = 0 ; i < fishes.Count; i++) { Vector2 force = Vector2.zero; Fish me = fishes[i]; force += bounding(me) + wander(me) + flocking(me,i); me.velocity *= 1.0f - drag * deltaTime; me.velocity += force * deltaTime; if (me.velocity.magnitude < minSpeed) { me.velocity = me.velocity.normalized * minSpeed; } else if (me.velocity.magnitude > maxSpeed) { me.velocity = me.velocity.normalized * maxSpeed; } me.position += me.velocity * deltaTime; fishes[i] = me; } }
粒子系统 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 for (int i = 0 ; i < fishes_Count; i++){ Fish fish = flock.fishes[i]; ParticleSystem.Particle particle = particles[i]; if (particle.position == Vector3.zero) { particle.position = fish.position; } particle.velocity = Vector2.Lerp(particle.velocity, fish.velocity, deltaTime * velocityLerpSpeed); particle.position += particle.velocity * deltaTime; particle.position = Vector2.Lerp(particle.position, fish.position, deltaTime * positionLerpSpeed); particle.startSize = particleSystemStartSize*fishSizeList[fish.type]; particle.startLifetime = 1.0f ; particle.startColor = fishColorList[fish.type]; particle.remainingLifetime = 1.0f ; particles[i] = particle; }
Boids算法 这种类鸟群算法叫做Boids,它本质上就是模仿了鸟群中鸟个体的三个最主要行为,即分离,对齐与聚合。
分离 个体会因为避免撞上其他个体(或是逃避)而产生一个远离其他个体的趋势,我们可以用邻居个体相对于该个体的方向向量direction来量化这个趋势。
分离的趋势应当越靠近越大,所以还当有一个距离因子ratio,使得ratio*direction向量能够相对于单单direction更加真实地模拟分离趋势,我们称这个向量为分离向量。
在一个分离区间separationRadius内,某个体的所有相邻个体给这个个体施加的分离向量和就是最终的分离向量。
加入捕食者后的逻辑:
个体遇到捕食者的分离区间应该更大seeSharkRadius(远远地看到捕食者就应当逃避),且有面对捕食者的恐惧因子sharkRatio加剧趋势。
捕食者不应因为靠近普通个体产生分离趋势,即捕食者不怕撞上普通个体
由此即可写出分离部分代码
1 2 3 4 5 6 7 8 9 10 11 12 float distance = (other.position - me.position).magnitude;Vector2 direction = (other.position - me.position) / distance; if (me.type > 0 && other.type == 0 && distance < seeSharkRadius){ separation -= direction * sharkRatio; } else if (distance < separationRadius && !(me.type == 0 && other.type > 0 )){ float ratio = 1.0f - (distance / separationRadius); separation -= direction * ratio; }
对齐 个体应当向着群体运动的大方向运动
为此我们应当计算对齐区间alignmentRadius内所有个体的方向的均值
这个均值代表的方向与个体方向的差值即是对齐的趋势,我们称之为对齐向量
加入捕食者后的逻辑:
捕食者往往不会扎堆行动,且往往不需要与食物对齐。普通个体更不需要与捕食者对齐,所以无论本身还是邻居是捕食者,都不需要被用来计算对其向量
普通个体必须是同一种群才需要对齐
循环中
1 2 3 4 5 6 7 if (other.type > 0 &&me.type>0 && distance < alignmentRadius&&me.type==other.type){ num_vel++; per_vel += other.velocity; }
循环外
1 2 3 4 5 if (num_vel > 0 ){ per_vel /= num_vel; alignment = (per_vel - me.velocity); }
聚合 个体应当向着群体的中心移动
为此我们应当计算聚合区间cohesionRadius内所有个体的位置的均值
这个均值位置相对于个体位置的方向即是聚合的趋势,我们称之为聚合向量
加入捕食者后的逻辑:
捕食者会因为所有普通个体而产生聚合趋势(被食物所吸引),且应该具有更大的聚合区间sharkCohRadius(更大的捕猎视野)
普通个体必须是同一种群才需要聚合
任何个体都不会因为捕食者个体产生聚合趋势,包括其他捕食者个体
循环中
1 2 3 4 5 if (distance < ((me.type > 0 )?cohesionRadius:sharkCohRadius)&&other.type > 0 &&(me.type == other.type||me.type==0 )){ num_pos++; per_pos += other.position; }
循环外
1 2 3 4 5 if (num_pos > 0 ){ per_pos /= num_pos; cohesion = (((per_pos - me.position))/(per_pos - me.position).magnitude); }
权重 将三个行为向量分配权重后即得到了个体的行为趋势,即代码结构 部分中我们提到的**”力”**
1 2 3 force += separation*separationForce + cohesion*cohesionForce + alignment*alignmentForce;
其它模拟 边界 unity模拟的时候鸟总是飞出摄像机边界怎么办?
加入一个边界向量,如果鸟超出了边界就给他一个返回的**”力”**
1 2 3 4 5 6 7 8 9 10 11 12 13 Vector2 origin = new Vector2(transform.position.x, transform.position.y); Vector2 lowerBound = origin - bounds * (boundsScale / 2.0f ); Vector2 upperBound = origin + bounds * (boundsScale / 2.0f ); Vector2 force = Vector2.zero; for (int i = 0 ; i < 2 ; ++i) if (me.position[i] < lowerBound[i]) force[i] = boundingForce; else if (me.position[i] > upperBound[i]) force[i] = -boundingForce; return force;
漫游 鸟群的行为在一开始就注定了?
加入一个漫游模拟使得鸟的运动具有随机性来模拟现实中鸟活动受外界的干扰
1 2 3 4 5 6 7 8 9 10 11 12 13 me.wanderAngle += Random.Range(-Mathf.Deg2Rad * wanderAngleJitter, Mathf.Deg2Rad * wanderAngleJitter); Vector2 newXAxis = new Vector2(me.velocity.normalized.y, -me.velocity.normalized.x); Vector2 circleOffset = (me.velocity.normalized * Mathf.Cos(me.wanderAngle) + newXAxis * Mathf.Sin(me.wanderAngle) )*wanderRadius; Vector2 wanderTarget = me.position + (me.velocity.normalized * wanderDistance); wanderTarget += circleOffset; Vector2 offsetToTarget = wanderTarget - me.position; return offsetToTarget * wanderForce;
现在鸟总是能在运动方向前一段距离进行范围(-wanderAngleJitter,wanderAngleJitter)度的随机偏移
这个随机偏移也是一个**”力”**
后记与开源 第一次使用unity,非常的不熟练,折腾了一个星期才完成。后面还将对其进行改造,争取做一个海底生态箱或者小游戏出来。
项目上传至我的仓库了,需要者自取SugarSong404/boids_simutate: unity boids (github.com) 。
以下部分写于两天后:
用三维的方式实现了,效果更加清晰,本来意图做一个游戏奈何美工与设计能力有限,先到此为止了。将来有机会再完成