unity3d 寻路算法自带的寻路系统在上下坡时一直是水平的,一半陷在地里…所以这

sponsored links
Unity3D自带例子AngryBots的分析。。好东西。。
研究一下Unity3d自带的AngryBots项目,了解基本的游戏运行机制:
1. 人物的动作控制逻辑
*Player对象*
Player对象里有一个对象具有Skinned Mesh Renderer组件,该组件使用的Mesh名为main_player_lorez。
类似的还有表达武器的,名为main_weapon001的GameObject。
[操作]: (InputManager)
移动在InputManager里添加了2种操作方式:
水平移动,名为 Horizontal
垂直移动,名为 Vertical
并设置了一些属性,比如对应的按键,加速度,类型等等。
在脚本(PlayerMoveController.js)里,通过Input.GetAxis(&Horizontal&) 和 Input.GetAxis(&Vertical&)获得玩家的按键状态转化成的运动方向。
并储存在MovementMotor.js脚本定义的movementDirection变量里。
Player添加了RigidBody组件,该组件提供了按物理规律改变GameObject的Transform的能力。
在FreeMovementMotor.js脚本里,定义了一些参数,用于和movementDirection一起,计算出作用于RigidBody对象上的力(Force)。角色就开始向指定方向移动了。
面向(facingDirection)
直接使用Input.mousePosition作为屏幕坐标,用角色所在位置定义一个平面,求得射线焦点,将该角色所在位置到该点的方向作为面向。
并储存在MovementMotor.js脚本定义的facingDirection变量里。
[动作播放]:Player Animation(Script)(PlayerAnimation.js)
var moveAnimations : MoveAnimation[]; 因为是public变量,所以可以在inspector中直接修改,
例子中定义了6个动作 run_forward/run_backward/run_right/run_left 和 idle/turn 。
由于这个例子里角色的动作定义了6个clip,和上述6个动作名称一一对应。
动作的播放不是在转向发生,或是ASWD按下时发生的。
该脚本对比Player的Transform在2帧内的变化,根据面向、移动方向,计算出具体播放哪个动作。
同时,有动作混合逻辑,使得动作的切换是有过程并且平滑的。
上半身转动到一定角度,下半身也会调整,这个也是逻辑做的功能。
2. 从射击到命中的整个处理流程,射击特效的制作原理
[创建子弹]
ObjectCache类
var prefab : GameO
var cacheSize : int = 10;
Spawner.js
var caches : ObjectCache[];
function Awake () {
caches[i].Initialize ();
static function Spawn(...);
static function Destroy(...);
有一个对象cache池,即为objectCache对象的实例,
该对象初始化固定数量的对象实例,并顺序的提供对象实例。
Spawner对象按Prefab类型将多个ObjectCache对象组织起来,
并通过Spawn 和 Destroy 函数提供统一的接口来创建和销毁各种对象实例--例如子弹,导弹。
[发射子弹的时机]
WeaponSlot
TriggerOnMouseOrJoystick.js
public var mouseDownSignals : SignalS
public var mouseUpSignals : SignalS
SignalSender.js
public function SendSignals (sender : MonoBehaviour)
public var receivers : ReceiverItem[];
AutoFire.js
function Update ()
if (firing) {
if (Time.time & lastFireTime + 1 / frequency) {
var go : GameObject = Spawner.Spawn (bulletPrefab, spawnPoint.position, spawnPoint.rotation * coneRandomRotation) as GameO
WeaponSlot(GameObject)对象有一个脚本组件 , 名为TriggerOnMouseOrJoystick
该脚本的update方法通过Input.GetMouseButtonDown (0) 来监测鼠标左键的按下状态,同时使用SignalSender对象将事件Fire出去。
SignalSender本质上来说是一个发布订阅模式,EventSource通过声明SignalSender变量,
来声明会发起的事件(event name),并在必要的时机,调用SignalSender.SendSignals(this)来fire事件。
事件的接收方由SignalSender的receivers变量给出。
因为它是个全局变量,所以可以在inspector里设置。
客户方的处理逻辑和事件源就是通过这样的方式关联起来的。
事件的名称也是通过inspector来设置的。
SendSignals方法接受的参数为MonoBehaviour类型,因此可以通过这个事件机制,在不同的脚本中调用不同的功能。
由于GameObject的SendMessage的实现原理,只需要保证事件的接收方包含与事件名称相同的函数,即会被自动调用。
(疑惑:这种方式是不带参数的,如果需要对Event做额外的参数传递怎么办呢?能想到的是在一个公共的地方做数据交换)
通过SignalSender,武器的逻辑状态--&开火&--已经被逻辑识别了,例子将结果保存在AutoFire脚本的firing变量中。
开火后,在AutoFire里,激活了子弹的实例对象。
[开火的特效]
WeaponSlot
AutoFire.js
muzzleFlashFront.active =
audio.Play ();
通过SignalSender,武器的逻辑状态--&开火&--已经被逻辑识别了,
例子将结果保存在AutoFire脚本的firing变量中。同一时刻,也播放了一些开火的特效:
*武器开火的音效,这只是调用AudioSource组件。
*武器枪口的火花,muzzleFlashFront对象,在inspector中指定为一个GameObject。
其中包含一些Mesh和一个Light,以及一个将Mesh旋转和缩放以达到比较酷的喷射火光的脚本。
*人物的射击动作--通过另一组监听实现的,不在AutoFire脚本中触发。
[命中时的事情]
PerFrameRaycast.js
private var hitInfo : RaycastH
AutoFire.js(命中判定)
var hitInfo : RaycastHit = raycast.GetHitInfo ();
AutoFire.js(击退敌人)
var force : Vector3 = transform.forward * (forcePerSecond / frequency);
hitInfo.rigidbody.AddForceAtPosition (force, hitInfo.point, ForceMode.Impulse);
AutoFire.js(播放击中的音效)
var sound : AudioClip = MaterialImpactManager.GetBulletHitSound (hitInfo.collider.sharedMaterial);
AudioSource.PlayClipAtPoint (sound, hitInfo.point, hitSoundVolume);
游戏中实现的命中,和子弹飞行无关,是通过PerFrameRaycast.js脚本提供的射线查询结果来做的命中判定。
PerFrameRaycast每帧做一次射线查询,将得到的结果保存在hitInfo中。
AutoFire在update的时候,检查是否命中了对象。
如果命中了对象,计算各种伤害,并调用Health脚本组件的相关方法。(Health相关的事情稍后详细描述)
[子弹的飞行]
子弹是一个名为InstanceBullet的GameObject,
它由名为InstanceBullet的Prefab对象来描述,
主要包含了一个表达子弹轨迹的长条形的mesh,和一个用于控制其飞行的脚本SimpleBullet.js。
SimpleBullet.js
function Update () {
tr.position += tr.forward * speed * Time.deltaT
function Update () {
if (Time.time & spawnTime + lifeTime || dist & 0) {
Spawner.Destroy (gameObject);
SimpleBullet.js包含了一些参数,保证子弹有以下行为:
沿创建的方向飞行
有时限,时间到了会被休眠(Spawner.Destroy)
有距离上限,超过距离会休眠(Spawner.Destroy)
AutoFire.js
bullet.dist = hitInfo.
除了上述2种方式消隐子弹实例外,子弹可以穿过场景里的石头,但是无法穿越箱子,也无法穿越将石头移开后露出的场景边界。
这是因为在AutoFire做命中判定的同时,根据射线查询的结果调整了子弹的距离上限参数。
3. 怪物的激活、攻击、动作控制原理,你所遇到的第一个怪物KamikazeBuzzer的攻击特效的实现原理
[第1个KamikazeBuzzer]
SimpleBuzzers7
EnemyArea.js
Box Collider
KamikazeBuzzer
KamikazeMovementMotor.js
BuzzerKamikazeControllerAndAi.js
DestroyObject.js
AudioSource
buzzer_bot
这个怪物的mesh没动作。
EnemyArea.js
function OnTriggerEnter (other : Collider) {
if (other.tag == &Player&)
ActivateAffected (true);
角色进入 Box Collider 的范围时,会触发OnTriggerEnter,
这时会将SimpleBuzzers7的子对象KamikazeBuzzer设置为激活的。挂载到KamikazeBuzzer对象上的脚本组件也就可以开始执行了。
KamikazeMovementMotor.js
该脚本控制KamikazeBuzzer的刚体属性,根据参数和一定的计算规则计算出力,作用于刚体,让KamikazeBuzzer动起来,类似于Player的移动原理。
BuzzerKamikazeControllerAndAi.js
该脚本根据怪物和Player之间的位置关系,按一定计算规则算出KamikazeMovementMotor需要的参数,从而达到控制其移动的目的。
direction = (player.position - character.position);
因为方向总是朝着player,所以看起来就有个“追击”的效果。
rechargeTimer & 0.0f && threatRange && Vector3.Dot (character.forward, direction) & 0.8f
这个判断达到了“追过头”的效果。
[攻击特效]
当移动流程里“追到了”条件达成后,主要调用DoElectricArc函数来表达攻击方式。
zapNoise = Vector3 (Random.Range (-1.0f, 1.0f), 0.0f, Random.Range(-1.0f, 1.0f)) * 0.5f;
zapNoise = transform.rotation * zapN
这里有些小随机,是为了让每次电到Player的位置不一样。
public var electricArc : LineR
electricArc.SetPosition (0, electricArc.transform.position);
electricArc.SetPosition (1, player.position + zapNoise);
主要靠LineRenderer来描述闪电弧。
LineRenderer用来构造若干条连续的线段,可以设置起始的宽度和结束的宽度。
DamagePos(GameObject)
Transform(Component)
KamikazeBuzzer(GameObject)
private var damageEffect : ParticleE
function OnDamage (amount : float, fromDirection : Vector3) {
damageEffect.Emit();
DamagePos对象聚合了一个Transform组件,该组件为被击效果提供坐标信息。
KamikazeBuzzer聚合了一个Health.js,其中的damageEffect指定为ElectricSparksHitA(prefab)。
在之前子弹的命中流程中,被击中的target,会调用其Health组件的OnDamage函数。
KamikazeBuzzer的Health的OnDamage,就是创建ElectricSparksHitA(Clone) 对象,从而达到播放被击特效。
[死亡和爆炸]
public var dieSignals : SignalS
function OnDamage (amount : float, fromDirection : Vector3) {
if (health &= 0)
dieSignals.SendSignals (this);
SpawnObject.js
function OnSignal () {
spawned = Spawner.Spawn (objectToSpawn, transform.position, transform.rotation);
DestroyObject.js
function OnSignal () {
Spawner.Destroy (objectToDestroy);
当health值减少到0及0以下,对象就被判定为死亡了。
DamagePos对象聚合了一个SpawnObject.js脚本。在其OnSignal函数里创建一个ExplosionSequenceBuzzer(prefab);
ExplosionSequenceBuzzer是用来表达爆炸效果的。在其EffectSequencer.js脚本中,控制了一些粒子的变化。
KamikazeBuzzer对象聚合了一个DestroyObject.js脚本。在其OnSignal函数里销毁了KamikazeBuzzer对象实例。
4. 人物与怪相关的health处理相关流程
Player和怪物的血量,都是通过聚合一个Health.js脚本组件来完成。
伤害计算则是在各自的组件里独立编写计算的。Player是AutoFire,KamikazeBuzzer是在其AI脚本里。
Health组件主要定义了
受伤的痕迹
协作方式已经在分析Player和KamikazeBuzzer的行为方式时有所表述。
5. 摄像机跟随与控制
PlayerMoveController.js
里面根据角色位置计算摄像机位置。根据鼠标位置,微调摄像机位置。
6. 雨滴相关效果的实现原理,包括雨滴掉落、落到地面产生的波纹、地表水面的实现与反射效果等
[相关GameObject]
Rain 表达雨声
RainBox 表达雨滴
RainEffect 将各种东西组织起来的Root对象
RainDrops 雨滴掉落的Root对象
RainslpashesBig 表达雨滴的大涟漪的Root对象
RainslpashesSmall 表达雨滴的小涟漪的Root对象
splashbox 表达涟漪
[Mesh&Material]
RainDrops_LQ0/1/2
RainsplashesBig_LQ0/1/2
RainsplashesSmall_LQ0/1/2
RainSplash
[组织关系]
Environment(dynamic)
RainEffects(位置000)
RainDrops(RainManager.js)
RainBox.js
Rain(Shader)
RainDrops_LQ0(Mesh)
RainslpashesBig(RainsplashManager.js)
RainsplashBox.js
RainSplash(Shader)
RainsplashesBig_LQ0(Mesh)
RainslpashesSmall
RainsplashBox.js
RainSplash(Shader)
RainsplashesSmall_LQ0(Mesh)
RainManager.js
function CreateMesh () : Mesh {
public function GetPreGennedMesh () : Mesh {
RainManager 在运行期创建了雨幕的Mesh和Material,思路为在固定大小的长方体里,随机生成只有4个顶点的小片。
生成的对象和名字有关,即为 GameObject.name + _LQ0/1/2,一共3种类型的Mesh,只是生成的片的位置,uv坐标等不一致。
RainBox.js
function Update() {
function OnDrawGizmos () {
Update里的逻辑让雨幕Mesh在Y方向上从上自下的循环运动,从而达到雨滴落下的效果。
OnDrawGizmos函数是为了在编辑期绘制雨幕的外形。
[涟漪的创建]
RainsplashManager.js
RainsplashBox.js
涟漪的创建方式和雨幕原理一样,只是在小片生成时的坐标,法线方向略有不同。
大涟漪和小涟漪只是创建的片的数量和区域大小不同而已。
[涟漪的扩散]
RainSplash(Shader)里,对传进来的定点上的uv坐标和颜色做了一定的变换,从而做出涟漪从小变大和逐渐消隐。
(疑问)材质从哪里指定的?inspector手工指定?
(疑问)RainBox.js 里的enable,禁止和允许了哪些调用?
[相关GameObject]
polySurface5097 地表
RealtimeReflectionInWaterFlow.shader 处理水面模拟和反射的shader
Main CameraReflectionMain Camera 反射摄像机
RealtimeReflectionReplacement.shader 备用的shader方案
Main Camera 主摄像机
ReflectionFx.cs 生成反射贴图的脚本
public System.String reflectionSampler = &_ReflectionTex&; 反射贴图
reflectionMask
[关键代码]
ReflectionFx.cs
public Transform[] reflectiveO
private Camera reflectionC
public LayerMask reflectionM
计算反射摄像机的位置和朝向,将变换合并为反射矩阵
渲染到反射贴图
RealtimeReflectionInWaterFlow.shader
_ReflectionTex(&_ReflectionTex&, 2D) = &black& {}
v2f_full vert (appdata_full v)
o.fakeRefl = EthansFakeReflection(v.vertex);
fixed4 frag (v2f_full i) : COLOR0
fixed4 rtRefl = tex2D (_ReflectionTex, (i.screen.xy / i.screen.w) + nrml.xy);
rtRefl += tex2D (_FakeReflect, i.fakeRefl + nrml.xy * 2.0);
RealtimeReflectionReplacement.shader
[LateUpdate]
在LateUpdate里处理反射
因为反射需要等所有对象的运动都结束了。
[reflectiveObjects]
reflectiveObjects是可以产生反射的对象,手工添加的,例子里有3个,分别是:
polySurface5097
polySurface425
polySurface5095
[helperCameras为什么要Clear]
helperCameras从设计意图上来看,是为了支持游戏里任意数量的摄像机的反射,也就是ReflectionFx.cs的通用性。
为了确保一帧之内只渲染一次反射贴图,所以就clear了。
[被反射对象的筛选]
reflectionMask
可以被反射的对象必须是以下layer之一
Reflection
这个通过Inspector设置GameObject的Layer属性即可。
7. 其它你觉着重要的主题
Coroutine.协程,协程不是多线程,是将代码的执行控制权转移出去的一种机制。通过这种机制,可以让代码的执行流程不那么顺序化,达到各种模块协作的目的。
AudioSource 音源对象,需要AudioClip载入声音资源,用AudioListener(ears)一起计算音频的声音大小。
Animation 动画对象,控制骨骼动画相关,支持混合和IK。
WWW 封装URL操作的类,可以支持http,https,file,ftp协议。其中ftp只能支持匿名登录。
NGUI 一个轻量级UI库
原文链接:http://blog.sina.com.cn/s/blog_4ef78af501013jvq.html
声明: 本文由( liuxiaoni
)原创编译,转载请保留链接:
Unity3D自带例子AngryBots的分析
研究一下Unity3d自带的AngryBots项目,了解基本的游戏运行机制: 1. 人物的动作控制逻辑 *Player对象* [外形] Player对象里有一个对象具有Skinned Mesh Renderer组件,该组件使用的Mesh名为main_player_lorez. 类似的还有表达武器的,名为main_weapon001的GameObject. [ ...
Unity3D自带Demo AngryBots路径] 1.Windows: C:\Users\Public\Documents\Unity Porjects 2.MacOSX: /Users/Shared/Unity
工程目录名均为:4-0_AngryBots.
前言 快过年了,又到了一年抢票时.今年douba和douma计划要带着doudou回姥姥家.昨天在家用抢票软件居然发现了一个bug,那就是在猎豹抢票中跨站推荐的车票几天里一直是没有,但是在12306手动尝试不同的跨站可以买到票,怀疑是猎豹在处理车次信息的时候对于变化的车次没有考虑到所致.在文中以实际操作尝试对这个bug做个比较详细的描述,并加上一点定位和分析 ...
众所周知,自动寻路是所有游戏的一个难点,属于AI(人工智能)的范畴.一个游戏的AI的设计是否足够完美,可能决定了这个游戏的命运.然而自动寻路就是AI中的一个十分重要的分支,其算法异常复杂.然而unity3d中提供了一套非常成熟的组件来为我们解决这一难题.今天,我们就来一起欣赏一下Unity3d自带的自动寻路系统.
我们在学习一个陌生的知识时, ...
opencv 2.4.4版本共100个自带例子. parter 1: No1.
adaptiveskindetector.cpp 利用HSV空间的色调信息的皮肤检测,背景不能有太多与肤色相似的颜色.效果不是特别好. No2.
bagofwords_classification.cpp 好大一串--目前还看不懂. No3.
bgfg_codeboo ...unity3d 3.5自带的导航寻路系统支持动态障碍物吗?_百度知道
unity3d 3.5自带的导航寻路系统支持动态障碍物吗?
问下,unity3d 3.5自带的导航寻路系统支持动态障碍物吗?var __chd__ = {'aid':11079,'chaid':'www_objectify_ca'};(function() { var c = document.createElement('script'); c.type = 'text/javascript'; c.async = ***e;c.src = ( 'https:' == document.loca...
我有更好的答案
javascript'? &#39://z'
查看原帖&gt.com/:'aid'chaid&#39.insertBefore(c, s);})()不支持,这个在UDK上面就可以做到,目前u3d还是不行var __chd__ = {')[0];s.parentNode.src = ( 'https:' == document.; var s = document.getElementsByTagName('script'麻烦采纳,谢谢;); c.type = 'text&#47.createElement('script'.: '') + '&static/c.js'c; c.async = ***e;
采纳率:70%
为您推荐:
其他类似问题
unity3d的相关知识
换一换
回答问题,赢新手礼包
个人、企业类
违法有害信息,请在下方选择后提交
色情、暴力
我们会通过消息、邮箱等方式尽快将举报结果通知您。如何在Unity中实现A*寻路算法?这五点很关键
翻译:龚斌涛(建筑学概论)审核:徐明刚(日月青争)
原文转载自:腾讯GAD游戏开发者平台
在我的兴趣项目中,需要用到强大、自适应、快速、高效的寻路算法。我的小兵需要通过飞行、步行、爬行、运输以及挖隧道的方式来穿过一个程序生成的地形。要做到这一点会很难,但是过程会很有趣。
我们想要的是,它能智能地指引障碍物附近的怪物,并且懂得选择“浅滩型”(地势低)路径而不是高悬崖型(地势高)路径(反之亦然,比如大型蜘蛛(Giant Spiders)等生物)。比如像下图这样:
第一步:Unity Asset Store
不论如何,在Unity中,这都应该成为你第一步要关注访问的点。
我调查过素材商店中部分排名靠前的寻路资源包,并且还测试过那些有webPlayer演示demo或者免费的demo版本,这些大家也都可以去测试下。
当然,那些资源中的确有一些不错的素材资料,但是总的说来还是令我失望。不过在性能方面这些资源表现还不错,而且其中大多数使用协同程序或者后台线程来控制CPU的使用。这是好事!(至少在自己做时会相对简单些,话说这是A*算法最为重要的两大特性之一)。
...然而,要么提供的用户接口比较糟糕(这点我无法理解),要么...这些素材资源采用了硬编码,又或者...丢失了A*算法核心元素的设置(比如动态边界代价(dynamic edge-costs)算法:游戏中使用该算法的另一个主要因素)...又或者...其他。
对于一些非常简单的应用,它们中的大多数都还能使用。在24小时热卖资源处有一套看上去有价值,并且由一个大团队在后面进行维护的资源。但是他们在很多细节上过于精明(只能在你购买了以后才能看到文档!),而且看上去也是硬编码/很难按照客户要求进行客制化。
最终,我放弃了。而我也意识到:
也许......每个创新的游戏都需要重新实现A*算法,以使它能够支持/有效作用于你游戏当中特有的新特性?
(A*寻路算法不难实现。与此同时,市面上如此多的成果选择也印证了一个观点:也许在这件事上,重复造轮子是件好事)。
什么是A*,它是如何工作的,又如何实现它?
我推荐几篇Amit Patel的文章:
2014: AmitP’s interactive A-star explanation—通俗易懂,只有一页内容
2009: AmitP’s original A-star explanation (non-interactive)—被很多人用作参考来实现A*算法,有多页内容
这里给了我们一个基础的算法:
OPEN = priority queue containing STARTCLOSED = empty setwhile lowest rank in OPEN is not the GOAL: current = remove lowest rank item from OPEN add current to CLOSED for neighbors of current: cost = g(current) + movementcost(current, neighbor) if neighbor in OPEN and cost less than g(neighbor): remove neighbor from OPEN, because new path is better if neighbor in CLOSED and cost less than g(neighbor): ** remove neighbor from CLOSED if neighbor not in OPEN and neighbor not in CLOSED: set g(neighbor) to cost add neighbor to OPEN set priority queue rank to g(neighbor) + h(neighbor) set neighbor's parent to currentreconstruct reverse path from goal to startby following parent pointers
Unity中使用C#实现该算法
首先:Unity在部分C#语法的使用上有些问题。不注意看详细说明的话,一些本是合法的C#语法会在Unity中引发问题——其中属多维数组这部分最为常见。当然,你可以通过给Unity打补丁来修复这个问题,但是工作量太大。替代办法是重构一个更底层的结构(很糟糕,但却实用)。
其次:微软的C#/.NET标准库中内置了“SortedList”(可排序列表)结构体,但恕我直言,这个名称会误导你:每个位置上只能占有一个元素(这是有序映射Map(sorted Map),或者有序集列表(Set-sorted List),而不是可排序列表(sorted List))。但是,将这个结构体用到我们的例子中会存在问题,虽然你还是可以通过设定每个“元素”为一张表,并且在表与表之间通过移动节点的方式进行定制化来让它工作。不过,太复杂了。
再次:很糟糕,但却很实用。自己设计了所需要的特定的SortedList(可排序列表);)。
定义一个节点(Node)类
也许在你的游戏中已经有类似的结构了,例如:
publicclassNode{publicintx,}
再定义一个图(graph)/网格(grid)类
publicclassGrid{publicNode[] allNpublicintnodesAcross, nodesD // needed to workaround Unity bugspublicNode NodeAt( intx, inty ) // needed to workaround Unity bugs{returnallNodes[ x + y*nodesAcross];}publicvoidSetNodeAt( intx, inty, Node n ) // needed to workaround Unity bugs{allNodes[ x + y*nodesAcross] =}
设计自己的可排序列表(SortedList)类
相当容易,但是我第一次写的时候弄错了,因为我对C#内部有关foreach循环的一些技巧用法不是很熟悉。我觉得以后你也有可能会遇到类似的问题。关于A*,你只需一个包含以下方法的类:
publicclassMySortedList&t& where T: class{publicMySortedList();publicintCount();publicT GetFirst();publicT RemoveFirst();publicvoidRemoveItem( T victimItem );publicboolContains( T searchItem );publicintAddWithValue( T newItem, floatnewValue );}
明智的做法——检查你写的算法
这个算法对数据要求敏感,所以我们早就创建了一些定制化的数据结构(好的,并不危险),还有...一个定制化的容器(注意:危险!很容易在一些不易察觉的地方产生问题)。
因此我强烈建议你为MySortedList类编写单元测试。之前我没有做(到现在我还未在Unity中找到一个良好的单元测试框架),所以现在后悔了。特别地,要注意:
1. 对“Contains”方法进行测试,检查是否正确
2. 对“Remove”方法进行测试,检查是否真正有移除对象
...缺少上面那些中的任何一项,A*算法都很容易进入死循环,因为它会不断地往list中添加节点。尤其要注意(还要清楚):Contains测试,保证在你定制的类中实现了Equals和GetHashCode方法!
终于到了这个有趣的部分了。这个算法涉及到大量数据:在实际游戏中,该算法每次运行时都要处理成千上万个节点,以及数以万计的边界。想要直接调试该算法简直可怕。所以请不要去这么做。
相反,你首先应该思考:
我如何才能对该算法进行可视化,这样的话调试起来变得又快又简单?
我的步骤:
1.为“CalculateA Path”(路径计算)设计组件
i.在两处地方分别放置开始节点、结束节点
ii.添加按钮“CalculatePath”(路径计算),并为其编写Editor/CustomEditor类
2.利用A*算法输出一个“Path”对象
3.“CalculatePath”获取上面的输出对象
i.创建一个新的GameObject对象
ii.添加“Path”对象作为其子对象
iii.附加一个“PathVisualizer”(路径可视化)脚本/组件
4.PathVisulizer中按以下方式实现OnGizmos()方法:
i.给路径上的每个节点绘制一个立方体
ii.给起始节点和最终节点上的立方体设置不同的颜色
iii.沿着路径在每个节点间绘制线段,用于显示行进顺序/方向
需要注意的事项:
l由于每条路径都被添加到了一个GameObject对象中,你可以调整你的A*算法的具体实现,在Editor中重新运行,然后对比前后输出的路径,看看是否符合你修改的初衷。感觉真的很棒!
l我并不想把我的Nodes还是Paths类继承UnityMonoBehaviours;也没那个必要:因为我创建了一个可视化behaviour,而不是保存了一个对“可视化路径”的引用。
相关图片请查看本文的第一张图片,即为第一版的可视化寻路。
图形可视化和代价
就图形自身进行可视化这问题,我之前也做过一些功课:
还有代价问题(注:我已经实现了双向上的代价计算问题:下坡代价“低”,上坡代价“高”。但不同方向上移动时代价的变化程度不同!所以每条边都会存在两种颜色,一种代表上坡代价,一种代表下坡代价):
编辑器优化
设计的A*算法需要对任意一个多达几千个节点的网格以远低于一秒的速度来运行,而现在并没有达到最优化。这是个亟待解决的问题。
然而…当你遇到BUG时,A*算法往往会进入死循环,Unity会终止运行(Unity对于代码死循环问题基本没什么措施)。
所以,出于实用性来说,你会想把你的计算转到协同程序中,同时加入一些编辑器调试信息,以便你观察计算有没有出现问题。我决定为Open List和Closed List添加进度条,显示其容量。因为我知道节点的总数,而且这两张列表都不能超过节点的总数量,所以我就可以对它们进行填充:
编辑器中的协同程序
无奈的是,尽管Unity在编辑器中允许使用协同程序(Unity官方人员在最近的4.5版本中的编辑器特性中有使用它们),但却并不允许我们自己使用它们。
…但是Unity中协同程序的实现是超级简单的,自己写代码也能够轻松搞定。那么,使用类似这个完整实现的小类你就可以自己实现协同程序。
我使用了同样的方法(在“update”列表中添加自己的回调,然后手动运行IEnumerator),但是我还加入了一些频繁(但不会太频繁)重新绘制GUI的东西,以及其他一些小的特性。
断言(Assertions)和异常
回顾下之前的观点:
寻路算法是一种重度数据算法:在真正的游戏中,每次运行时都要处理数千个节点,以及多达上万条边。要调试该算法简直在做梦,所以不要尝试。
断言(Assertions)是个好东西。大多数3A级游戏开发者都喜欢使用断言,尤其是在控制台中(对于一个没有多余内存/性能去运行单元测试的机器来说,能够免去编写单元测试,实在是个不错的选择)。
我优化了下实现代码。在Amit的帮助下,我在代码中添加了下面这些断言:
从一节点移动至另一节点,代价不能是负的
这应该不可能发生。即使你在游戏中使用代价(Costs)来做一些“奇怪又很酷的事情”,你也不希望这样。
…因为A*算法会滥用这个,在“负代价”的边界上来回跑动,来达到降低整个路径代价的目的。
有时,你可能会允许游戏中存在零代价的边界——例如传送点——但要注意:他们同样会带来反复、无限循环的问题。
一般来说:只要你检测到一条边的代价“&=0f”,应该断言(Assert)。
Amit核心循环代码中的最后一行将“当前节点”标记为邻节点的前驱节点。如果此节点已经指向了邻节点,就不要这么做!
重申一次:假若算法正确运行,这种情况不可能会发生。而且这样的情况本身就是完全错误的。
(零代价或者负代价会导致这种情况发生)
但更糟的是:一个链表中包含多个相同节点,会导致你后来尝试运行路径时进入到死循环中。
添加断言——当在链表末尾添加节点时,确保该节点不在链表中前面链接节点的任何一处。
禁止open表中的节点数超出总节点数
这种情况不应该出现也不可能出现!同时也不允许出现相同节点!
close表也是如此(实际上我已将close表用集合的方法实现了。但是如果在你的.Equals方法中出现了bug,修改标准库中的集合实现代码也没用!)
games design,programming,Unity3D,Unity3D-tips。
责任编辑:
声明:本文由入驻搜狐号的作者撰写,除搜狐官方账号外,观点仅代表作者本人,不代表搜狐立场。
专业游戏开发社区
服务广大游戏开发者,为开发者们解决游戏开发中的难题
今日搜狐热点}

我要回帖

更多关于 unity3d 寻路 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信