移动游戏战斗系统实现方式探讨

我认为现在战斗系统需要满足一下几点。

1、一定要有离线PVE玩法,或者离线PVP玩法,可以在让玩家在网络不好的时候消遣,节省流量。(全民超神、王者荣耀在5V5匹配时候都有一定几率匹配到离线战斗,这个时候是不耗流量的,其他人全是AI控制的)

2、一定要有在线PVP,在线PVE,能够让玩家在网络比较好的时候,实时竞技。增加可玩性。

3、战斗中,尽最大程度节省玩家的流量,例如全民超神这款游戏,一场30分钟的战斗基本上要消耗掉20M的流量,而且此类游戏大部分是玩的联网战斗,基本上在非wifi情况下没法玩。

4、需要有战斗回放机制,可以让策划设计离线玩法的时候更自由,例如COC,战斗回放基本变成了它游戏的一部分。

5、防作弊,如果有离线玩法的话,一定有机制对离线玩法的结果进行验证,要不然等你游戏真火了,你就知道错了。

6、实现难度相对较低。

对于联网游戏来讲,同步的方式主要分为两种,状态同步、帧同步。

1、状态同步:顾名思义,是指的将其他玩家的状态行为同步的方式,一帮情况下AI逻辑,技能逻辑,战斗计算都由服务器运算,只是将运算的结果同步给客户端,客户端只需要接受服务器传过来的状态变化,然后更新自己本地的动作状态、Buff状态,位置等就可以了,但是为了给玩家好的体验,减少同步的数据量,客户端也会做很多的本地运算,减少服务器同步的频率以及数据量。

2、 帧同步:RTS游戏常采用的一种同步技术 ,上一种状态同步方式数据量会随着需要同步的单位数量增长,对于RTS游戏来讲动不动就是几百个的单位可以被操作,如果这些都需要同步的话,数据量是不能被接受的,所以帧同步不同步状态,只同步操作,每个客户端接受到操作以后,通过运算可以达到一致的状态(通过随机种子保证所有客户端随机序列一致),这样的情况下就算单位再多,他的同步量也不会随之增加。

下面我们从以上的5个方面对各自实现方式进行描述:

总结一下:

1、对于回合制战斗来讲,其实选用哪种方式实现不是特别重要了,因为本身实现难度不是很高,采用状态同步也能实现离线战斗验证。所以采用帧同步的必要性不是很大。

2、对于单位比较多的RTS游戏一定是帧同步,对于COC来讲,他虽然是离线游戏,但是他在一样输入的情况下是能得到一样结果的,所以也可以认为他是用帧同步方式实现的战斗系统。

3、对于对操作要求比较高的,例如MOBA类游戏有碰撞(玩家、怪物可以互相卡位)、物理逻辑,纯物理类即时可玩休闲游戏,帧同步实现起来比较顺畅,(有开源的Dphysics 2D物理系统可用 它是Determisti的)。

4、对于战斗时大地图MMORPG的,一个地图内会有成千上百的玩家,不是小房间性质的游戏,只能使用状态同步,只同步自己视野的状态。

5、帧同步有个缺点,不能避免玩家采用作弊工具开图。

 

  • MOBA
    王者荣耀:帧同步
    WAR3:帧同步
    全民超神:状态同步
    乱斗西游:状态同步
    LOL:状态同步
    Dota2:状态同步
    MMO
    MMO均采用状态同步,如魔兽世界
    FPS
    FPS类游戏大多采用状态同步
    动作格斗类游戏
    街机三国:帧同步
    体育竞技类游戏
    NBA2K OL:帧同步
    网络游戏同步“大”分类
  • 什么是帧同步?
    War3/星际等采用,基于指令驱动各个客户端自计算逻辑。服务端只管分发指令,每个客户端根据完整的规则运算整个战场。
    每个客户端播放效果就像是看视频。
    对视觉同步要求较高。
  • 核心
    保证所有客户端每帧的输入都一样–同步性
    相同的输入下要有相同的输出–确定性
  • 优点
    因为只需要同步指令,所以流量消耗非常小
    缺点
    断线需要补帧,即一旦你的战场状态没有了,必须从头开始,从第一个指令运算到连回去的战场状态。
  • 什么是LockStep?
    典型的一种帧同步算法,最早用于P2P。Lockstep把游戏过程划分成了一个个turn,只有当每个turn集齐了所有玩家的操作指令,也就是输入确定了之后,才可以进行计算,进入下一个turn,否则就要等待最慢的玩家。
    使用Lockstep的游戏是严格按照turn向前推进的,如果有人延迟比较高,其他玩家必须等待该玩家跟上之后再继续计算,不存在某个玩家领先或落后其他玩家若干个turn的情况。使用Lockstep同步机制的游戏中,每个玩家的延迟都等于延迟最高的那个人。
  • 什么是Timebucket?
    设置了 bucket的概念, 执行每一帧的时间是固定的 bucket 时间节点, 而不必等到收到所有的 client step 指令, 从而网络不再受最差的 client 限制.
  • 什么是状态同步?
    MMORPG的主要实现方式,服务器负责计算全部的游戏逻辑,并且广播这些计算的结果,客户端仅仅负责发送玩家的操作,以及表现收到的游戏结果。一般来说,玩家发送一个操作到服务器上,服务器根据玩家操作去修改内存中的游戏世界模型,同时运算游戏世界对这个操作的反应,然后把这些反应都广播给相关的多个客户端,每个客户端负责把这些数据表现出来给玩家看。
    一句话,The Server is the Man!
  • 什么是DR?
    Dead Reckoning,导航推测算法,即客户端模拟运动轨迹和路线,如果真实坐标和模拟坐标的差值大于某个极限误差的时候则广播,收到消息后进行平缓的拉扯处理。
    属于客户端预测。
  • 什么是Timewarp?
    客户端先行,发现逻辑不一致的时候,进行回滚。
  • 什么是延迟补偿?
    服务器端考虑了客户端的网络延迟,将服务器状态回滚到延迟前,再进行运算。
  • 什么是延时、抖动、丢包率?
    ping kingsoft.cn
    正在 Ping kingsoft.cn [192.168.12.19] 具有 32 字节的数据:
    来自 192.168.12.19 的回复: 字节=32 时间=5ms TTL=123
    来自 192.168.12.19 的回复: 字节=32 时间=6ms TTL=123
    来自 192.168.12.19 的回复: 字节=32 时间=5ms TTL=123
    请求超时。
    192.168.12.19 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 3,丢失 = 1 (25% 丢失),
    往返行程的估计时间(以毫秒为单位):
    最短 = 4ms,最长 = 6ms,平均 = 5ms
    ______________________________________
    注:延时5ms
    抖动:(4ms-5ms)~(6ms-5ms),即-1ms~+1ms,[最短延时-平均延时] ~ [最长延时-平均延时]
    丢包率:25%
  • 如何选择协议?
    TCP vs. UDP
    乱斗西游采用的是UDP
    NBA2K OL存在用的是UDP
    补充一句:使用哪种同步方式和协议方式没有必然联系;比如乱斗西游是UDP+状态同步,而街机三国是TCP(TCP_NODELAY) + 帧同步
    注:可参考本人的上一篇文章:Networking Basics:TCP and UDP Basics
  • 帧同步如何防外挂?
    如果初始状态一样和随机种子一样,那么只要每帧的输入高度一样(因为是由服务器切帧分发的,可以保持每个客户端的输入序列是一样的),那么每帧的运算结果也是一样的。为此让每个客户端验证每帧的结果是不是与其它几个客户端是一模一样的,我们就可以拿来做为校验原因。只要有不一样的结果,即有人做弊。
  • 为什么MMO不能用帧同步?
    帧同步的[所有人]的输入必须在[所有客户端]进行计算,这样大家运算出来的结果才能一样,才能保证帧同步.注意是【所有人】.所以常见用于rts,moba、2k等房间游戏.因为人数固定.但是mmo中玩家不定,如果同步所有人。。这个带宽(即传输问题)。。所以mmo中用状态同步+aoi视野管理.

 

参考资料:
lockstep 帧同步
RTS游戏
determistic 的unity 物理引擎
lockstep framework
乱斗西游技术分享

模拟重力制作抛物线

模拟重力制作抛物线

public class NewBehaviourScript : MonoBehaviour {


 public Transform pointA;//点A
 public Transform pointB;//点B

 public float ShotSpeed;
 public float g = 1;//重力加速度
 // Use this for initialization
 private Vector3 speed;//初速度向量
 private Vector3 Gravity;//重力向量

 private float dTime; //
 private float time;


 private Vector3 currentDirection;
 public void Start()
 {
 dTime = 0;
 transform.position = pointA.position;//将物体置于A点
 Gravity = Vector3.zero;//重力初始速度为0

 currentDirection = Vector3.down;
 }

 public void FixedUpdate()
 {
 time = Vector3.Distance(pointA.position, pointB.position) / ShotSpeed;//代表从A点出发到B经过的时长
 //通过一个式子计算到顶点时的速度
 speed = new Vector3((pointB.position.x - pointA.position.x) / time,//x是水平方向移动速度
 (pointB.position.y - pointA.position.y) / time + g * time * 0.5f, 0);//y是垂直方向的移动速度

 Gravity.y = g * (dTime += Time.fixedDeltaTime);//改值是由小到大逐渐递增的

 var lastPosition = transform.position;
 //模拟位移
 transform.position += (speed - Gravity) * Time.fixedDeltaTime; //当dtime小于time * 0.5f向上运动的.等于的时候到达顶点.大于的时候向下运动

 var targetDirection = (transform.position - lastPosition).normalized;
 //因为投射物体的箭头是朝下的.所以用Vector3.down.计算和目标的角度.然后旋转
 Vector3 cross = Vector3.Cross(currentDirection, targetDirection);
 var angle = Vector3.Angle(currentDirection, targetDirection);
 angle = cross.z > 0 ? angle : -angle;
 this.transform.Rotate(0, 0, angle);
 currentDirection = targetDirection;

 if (dTime > time)
 {
 dTime = 0;
 Gravity = Vector3.zero;
 transform.position = pointA.position;
 }
 }

 
}

 

 

 

 

 

 

 

 

 

 

根据抛物线公式画抛物线

抛物线公式:

//抛物线公式y=a*x*x+b*x+c;a>0,开口向上; a<0, 开口向下。b=0, 抛物线对称轴为y轴。c=0, 抛物线经过原点。
 //通过修改a的值来改变小山峰的陡峭程度。
 public float a = -1 ;//a>0,开口向上;a<0,开口向下。 
 public int size = 22;
 public Transform goA;
 public Transform goB;
 public Transform goC;
 // Draw the line of sight representation within the scene window
 public void OnDrawGizmos()
 {
 var oldColor = UnityEditor.Handles.color;
 var color = Color.red;
 UnityEditor.Handles.color = color;

var v = new Vector3[size];

var dire = (goB.position - goA.position).normalized;
 var target = Vector3.right;
 
 Vector3 cross = Vector3.Cross(dire, target);
 var angle = Vector3.Angle(dire, target);//不分正负的...判断是否在视野内不需要..其他的地方需要
 angle = cross.z > 0 ? angle : -angle; //求出向量旋转角度 
 if (goC == null) 
 {
 goC = GameObject.Instantiate(goB);
 }

goC.position = goA.position + Quaternion.Euler(0,0,angle)* (goB.position - goA.position);//a-b点的向量旋转后的值
 //goC.RotateAround(goA.position, Vector3.forward, angle); //另一种求goc旋转x度的位置的方法.不过该方法会旋转c

var b = -(Mathf.Abs(goC.position.x) - Mathf.Abs(goA.position.x))/2*2*a;//对称轴距离Y轴的偏移量,根据对称轴x的值使用公式x=-b/2a,逆推出b的值
 var c = goA.position.y - goA.position.x * goA.position.x * a - b * goA.position.x;//逆推出c的值

var step = (goC.position.x - goA.position.x) / size;
 for (var i = 0; i < size; i++){
 float x = goA.position.x + step*i;

Vector3 p = new Vector3();
 p.x = x;
 p.y = a * x * x + b * x + c;
 p.z = 0;
 // v[i] = p;
 v[i] = goA.position + Quaternion.Euler(0, 0, -angle) * (p-goA.position); //将坐标点旋转为正常的值
 }

UnityEditor.Handles.DrawLines(v);
 UnityEditor.Handles.color = oldColor;
 }

 

抛物线顶点公式

 //y=a(x-h)²+k
 public float height = 2;//a>0,开口向上;a<0,开口向下。 
 public int size = 22;
 public Transform goA;
 public Transform goB;
 public Transform goC;
 // Draw the line of sight representation within the scene window
 public void OnDrawGizmos()
 {
 var oldColor = UnityEditor.Handles.color;
 var color = Color.red;
 UnityEditor.Handles.color = color;

 var v = new Vector3[size];

 var dire = (goB.position - goA.position).normalized;
 var target = Vector3.right;

 Vector3 cross = Vector3.Cross(dire, target);
 var angle = Vector3.Angle(dire, target);//不分正负的...判断是否在视野内不需要..其他的地方需要
 angle = cross.z > 0 ? angle : -angle; //求出向量旋转角度

 var oldPosition = goB.position;

 if (goC == null)
 {
 goC = GameObject.Instantiate(goB);
 }

 goC.position = goA.position + Quaternion.Euler(0, 0, angle) * (goB.position - goA.position);//a-b点的向量旋转后的值
 
 //goC.RotateAround(goA.position, Vector3.forward, angle); //另一种求goc旋转x度的位置的方法.不过该方法会旋转c

 var center = goA.position+ (goC.position - goA.position) / 2;
 //求出顶点
 var h = center.x;
 var k = center.y+height;

 UnityEditor.Handles.DrawLine(goA.position, goC.position);
 UnityEditor.Handles.DrawLine(center, new Vector3(h,k,0));

 var a = (goA.position.y - k)/((goA.position.x-h)* (goA.position.x - h)); //根据顶点公式.反推a

 var step = (goC.position.x - goA.position.x) / size;
 for (var i = 0; i < size; i++)
 {
 float x = goA.position.x + step * i;

 Vector3 p = new Vector3();
 p.x = x;
 p.y = a * (x - h) * (x - h) + k;
 p.z = 0;
 // v[i] = p;
 v[i] = goA.position + Quaternion.Euler(0, 0, -angle) * (p - goA.position); //将坐标点旋转为正常的值
 }

 UnityEditor.Handles.DrawLines(v);
 UnityEditor.Handles.color = oldColor;
 }

 

 

 

 

Vector3函数理解-计算两向量之间的角度

1.已知两个向量dirA,dirB。
Vector3 dirA = new Vector3(-1,1,0);

Vector3 dirB = new Vector3(-1,1,1);
2.使向量处于同一个平面,这里平面为XZ

dirA = dirA – Vector3.Project(dirA,Vecotr3.up);
dirB = dirB – Vector3.Project(dirB,Vecotr3.up);
注:Vector3.Project计算向量在指定轴上的投影,向量本身减去此投影向量就为在平面上的向量
3.计算角度
float angle = Vector3.Angle(dirA,dirB);

4.计算方向
float dir = (Vector3.Dot (Vector3.up, Vector3.Cross (dirA, dirB)) < 0 ? -1 : 1);
angle *= dir;

Vector3.Cross 叉乘返回为同时垂直于两个参数向量的向量,方向可朝上也可朝下,由两向量夹角的方向决定。
Vector3.Dot 点乘意义为两参数向量方向完全相同返回1,完全相反返回-1,垂直返回0。当两向量角度减小,将得到更大的值。

判断目标是不是在攻击范围内

 

一个点是否在扇面上只需要判定 目标点与原点的距离是否大于半径&&目标点与原点朝向的角度在扇形角度中 就可以了

第一步:以玩家为原点向玩家朝向给定一个已知距离的向量
第二步:给定扇形角度,已第一步得到的向量作为中分轴
第三步:求目标点与角色的距离得到向量并判定距离是否大于半径
第四部:由第一步和第三步的向量得到角度 Vector3.Angle()
第五步:判定角度绝对值是否大于第二步规定的扇形角度。
如果第三步和第五步都满足,就是在咯。

 


关于碰撞器和触发器

要产生碰撞必须为游戏对象添加刚体(Rigidbody)和碰撞器,刚体可以让物体在物理影响下运动。碰撞体是物理组件的一类,它要与刚体一起添加到游戏对象上才能触发碰撞。如果两个刚体相互撞在一起,除非两个对象有碰撞体时物理引擎才会计算碰撞,在物理模拟中,没有碰撞体的刚体会彼此相互穿过。
物体发生碰撞的必要条件
两个物体都必须带有碰撞器(Collider),其中一个物体还必须带有Rigidbody刚体。
在unity3d中,能检测碰撞发生的方式有两种,一种是利用碰撞器,另一种则是利用触发器。
碰撞器:一群组件,它包含了很多种类,比如:Box Collider(盒碰撞体),Mesh Collider(网格碰撞体)等,这些碰撞器应用的场合不同,但都必须加到GameObjecet身上。
触发器,只需要在检视面板中的碰撞器组件中勾选IsTrigger属性选择框。
触发信息检测:
1.MonoBehaviour.OnTriggerEnter(Collider collider)当进入触发器
2.MonoBehaviour.OnTriggerExit(Collider collider)当退出触发器
3.MonoBehaviour.OnTriggerStay(Collider collider)当逗留触发器
碰撞信息检测:
1.MonoBehaviour.OnCollisionEnter(Collision collision) 当进入碰撞器
2.MonoBehaviour.OnCollisionExit(Collision collision) 当退出碰撞器
3.MonoBehaviour.OnCollisionStay(Collision collision)  当逗留碰撞器
 当发生碰撞反应的时候,会先检查此Is Trigger属性。
当Is Trigger=true时,碰撞器被物理引擎所忽略,没有碰撞效果,可以调用OnTriggerEnter/Stay/Exit函数。
当Is Trigger=false时,碰撞器根据物理引擎引发碰撞,产生碰撞的效果,可以调用OnCollisionEnter/Stay/Exit函数;
总结:Is Trigger 好比是一个物理功能的开关, 是要“物理功能”还是要“OnTrigger脚本”。

基于两个碰撞对象的配置,可以产生很多不同的效果。下表概括了基于附加不同组件的两个碰撞对象所产生的效果。其中有些组合只能导致碰撞的两个对象中的一个受到影响,.所以考虑到保持标准的规则-物理效果将不会对没有附加刚体的对象生效。

Collision detection occurs and messages are sent upon collision
Static Collider Rigidbody Collider Kinematic Rigidbody Collider Static Trigger Collider Rigidbody Trigger Collider Kinematic Rigidbody Trigger Collider
Static Collider Y
Rigidbody Collider Y Y Y
Kinematic Rigidbody Collider Y
Static Trigger Collider
Rigidbody Trigger Collider
Kinematic Rigidbody Trigger Collider
Trigger messages are sent upon collision
Static Collider Rigidbody Collider Kinematic Rigidbody Collider Static Trigger Collider Rigidbody Trigger Collider Kinematic Rigidbody Trigger Collider
Static Collider Y Y
Rigidbody Collider Y Y Y
Kinematic Rigidbody Collider Y Y Y
Static Trigger Collider Y Y Y Y
Rigidbody Trigger Collider Y Y Y Y Y Y
Kinematic Rigidbody Trigger Collider Y Y Y Y Y Y

 


射线和碰撞器

射线是3D世界中一个点向一个方向发射无终点的线。

在unity3d中我们发射的射线一旦与其他的碰撞器(不需要添加刚体组件)发生碰撞,射线将停止发射。在游戏制作过程中我们可以通过判断射线是否发生了碰撞,并且可以判断射线和谁发生了碰撞。应用范围非常广泛,如射击类游戏中用它来判断是否射中目标。

我们要想在游戏中发射一条射线,必须要有两个元素,一个起始点,一个方向。

Ray.origin:射线起点

Ray.direction:射线的方向

创建一条射线的方法Ray (origin : Vector3, direction : Vector3)

Origin是射线的起点,direction是射线的方向。

首先在场景中创建一个CUBE,创建一个c#文件,并输入如下代码:

 

 void Update ()

       {

          //定义一条射线,起点为Vector3.zero终点为物体坐标

          Ray ray=new Ray(Vector3.zero,transform.position);

          //定义一个光线投射碰撞

          RaycastHit hit;

          //发射射线长度为100

          Physics.Raycast(ray,out hit,100);

          //在Scene中生成这条射线,起点为射线的起点,终点为射线与物体的碰撞点

          Debug.DrawLine(ray.origin,hit.point);   

       }

创建等距世界:游戏开发入门

在本教程中,会让你知道要创建的等距世界的广泛概述。你将学习什么是等角投影,以及如何用二维数组表示等距水平。我们会制定视图和逻辑之间的联系,这样我们就可以很容易的操纵屏幕上的对象,处理区块碰撞检测。我们也考虑深度排序和角色动画。


1.等距世界

等距视图是一种用来为2D游戏-有时也被称为伪3D或2.5D,创建3D错觉的显示方法。这些图片(图片来自最初的暗黑破坏神和帝国时代游戏)说明了我的意思:

diablo.png
暗黑破坏神
AOE.png
帝国时代

实施等距视图有很多方法,但为了简单起见,我将重点放在一个基于区块的方法,这是最有效、使用最广泛的方法。我上面的截图已经覆盖了一个菱形网格将地形分成区块。


2.基于区块的游戏在基于区块的方法中,每个视觉元素都被分解成小块,称为tile,具有标准的尺寸。这些tile将根据预先确定的水平数据-通常是一个二维数组,被用来形成游戏世界。
相关文章
Tony Pa’s tile-based tutorials

例如,让我们考虑一个有两个tile的标准俯视图-草tile和墙tile的2D视图-如下图所示:
base-2d-tiles.png
一些简单的tile

这些tile大小都一样,而且都是正方形,所以tile的高度和宽度是相同的。

表示四面八方的墙壁围着草场的数据层的二维数组,看起来像这样:

  1. [[1,1,1,1,1,1],
  2. [1,0,0,0,0,1],
  3. [1,0,0,0,0,1],
  4. [1,0,0,0,0,1],
  5. [1,0,0,0,0,1],
  6. [1,1,1,1,1,1]]

复制代码

在这里,0表示草tile,1表示墙tile.根据数据层排列tile会产生以下图像:
2d-level-simple.png

我们可以通过添加拐角tile和独立的垂直和水平墙tile来增强它,需要另外5个tile:

  1. [[3,1,1,1,1,4],
  2. [2,0,0,0,0,2],
  3. [2,0,0,0,0,2],
  4. [2,0,0,0,0,2],
  5. [2,0,0,0,0,2],
  6. [6,1,1,1,1,5]]

复制代码

2d-level-complex.png

我希望现在基于区块的方法的概念是明确的。这是一个简单的二维网格实现,我们的代码可以像这样:

  1. for (i, loop through rows)
  2. for (j, loop through columns)
  3.   x = j * tile width
  4.   y = i * tile height
  5.   tileType = levelData[i][j]
  6.   placetile(tileType, x, y)

复制代码

在这里,我们假设tile高度和宽度是相等的(相同的tile),tile图像的尺寸相匹配。所以在这个例子中的tile的宽度和高度均为50px,这使得总水平尺寸为300x300px-即6行6列的tile,每个tile尺寸50x50px.

在一个普通的基于区块的方法中,我们可以实现俯视图或侧视图,要实现等距视图,我们需要创建等角投影。


3.等角投影
关于“等角投影”最好的技术解释,我认为,是出自Clint Bellanger的这篇文章

  1. “ 首先(以Z轴为主轴)整体顺时针旋转90度,那么所有边都与X轴成45度。然后(以X轴为主轴)旋转60度,也就是下面点向自己,上面的点向外,整个面跟Z轴成30度,这样我们 就能看到投影的高刚好是原来的高的一半 ,这种方式通常用于战略游戏和动作RPG游戏,在这个视图中看一个立方体我们会看到三个面。(顶部和两个相对的侧面)”

复制代码

虽然这听起来有点复杂,实际上做到这一点很简单。我们需要了解的是二维空间和等距空间之间的关系-也就是说,数据层和视图的关系,数据层是根据俯视图的笛卡尔坐标转换的等距坐标。
the_isometric_grid.jpg

(我们不考虑基于六角形tile的技术,那是实现等距世界的另一种方法)

放置等距tile

让我们尽量简化数据存储为一个二维数组和等距视图之间的关系-那就是,我们如何将笛卡尔坐标转换为等距坐标。

我们试着为墙壁围着草地数据创建等距视图。

  1. [[1,1,1,1,1,1],
  2. [1,0,0,0,0,1],
  3. [1,0,0,0,0,1],
  4. [1,0,0,0,0,1],
  5. [1,0,0,0,0,1],
  6. [1,1,1,1,1,1]]

复制代码

在这种情况下,我们可以通过检查该坐标的数组元素是否为0,来表示草,以确定为可通行区域。2D视图实现上述情况使用一个简单的双重循环,将正方形tile放置到固定的高度和宽度值的位置。

  1. for (i, loop through rows)
  2. for (j, loop through columns)
  3. x = j * tile width
  4. y = i * tile height
  5. tileType = levelData[i][j]
  6. placetile(tileType, x, y)

复制代码

对于等距视图,该代码是相同的,但placeTile()函数改变。

对于等距视图,我们需要计算出相应的内部循环等距坐标。

下面方程式做到这一点,其中isoX和isoY代表等距坐标的x和y坐标,,cartX和cartY表示笛卡尔坐标的x和y坐标。

  1. //Cartesian to isometric:
  2. isoX = cartX – cartY;
  3. isoY = (cartX cartY) / 2;
  4. //Isometric to Cartesian:
  5. cartX = (2 * isoY isoX) / 2;
  6. cartY = (2 * isoY – isoX) / 2;

复制代码

这些函数向你展示如何从一个体系转换到另一个

  1. function isoTo2D(pt:Point):Point{
  2.   var tempPt:Point = new Point(0, 0);
  3.   tempPt.x = (2 * pt.y pt.x) / 2;
  4.   tempPt.y = (2 * pt.y – pt.x) / 2;
  5.   return(tempPt);
  6. }
  7. function twoDToIso(pt:Point):Point{
  8.   var tempPt:Point = new Point(0,0);
  9.   tempPt.x = pt.x – pt.y;
  10.   tempPt.y = (pt.x pt.y) / 2;
  11.   return(tempPt);
  12. }

复制代码

伪代码的循环看起来像这样:

  1. for(i, loop through rows)
  2.   for(j, loop through columns)
  3.     x = j * tile width
  4.     y = i * tile height
  5.     tileType = levelData[i][j]
  6.     placetile(tileType, twoDToIso(new Point(x, y)))

复制代码

isolevel-screenshot.png
墙壁围着草地的等距视图

让我们看到一个二维位置被转换为等距位置的典型的一个例子,

  1. 2D point = [100, 100];
  2. // twoDToIso(2D point) will be calculated as below
  3. isoX = 100 – 100; // = 0
  4. isoY = (100 100) / 2;  // = 100
  5. Iso point == [0, 100];

复制代码

同样的,输入[0,0]会得到[0,0] ,[10,5]将得到[5,7.5] 。

上面的方法,使我们能够创建2D水平数据和等距坐标之间的直接关系。我们看到使用此函数将tile的笛卡尔坐标转换为水平坐标数据。

  1. function getTileCoordinates(pt:Point, tileHeight:Number):Point{
  2.   var tempPt:Point = new Point(0, 0);
  3.   tempPt.x = Math.floor(pt.x / tileHeight);
  4.   tempPt.y = Math.floor(pt.y / tileHeight);
  5.   return(tempPt);
  6. }

复制代码

(在这里,我们基本上认为tile的宽度和高度是相等的,在大多数情况下。)

因此,根据一对等距坐标,我们通过调用可以找到tile坐标。

  1. getTileCoordinates(isoTo2D(screen point), tile height);

复制代码

screen point表示,可以用鼠标点击位置和获取位置。

提示
另一种方法是 Zigzag model,这是一种完全不同的方法。

移动等距坐标

运动是很简单的,你只是使用上述函数在笛卡尔坐标系中操作你的游戏世界数据,在屏幕上更新。例如,假如你想沿着Y轴正方形移动一个字符,你可以简单的增加Y值,然后将其转换为等距坐标位置。

  1. y = y speed;
  2. placetile(twoDToIso(new Point(x, y)))

复制代码

深度排序

除了正常的位置,我们需要考虑绘制等距世界的深度排序。这可以确保项目中绘制的元素更真实。

简单的深度排序方法就是使用笛卡尔y坐标值,Quick Tip中提到:屏幕上深一层的对象,应该先被绘制。只要我们没有任何一个sprite占据超过一个单一tile空间就可以了。

等距世界的深度排列的最有效方法就是要不允许更大的图像,将其全部分解成标准tile尺寸。例如,这里是一个不适合标准tile尺寸的图像–看我们如何把它分解成多个适合tile尺寸的tile:
split-big-tile.png

4.创建艺术

等距艺术可以是像素艺术,但这并不是必须的。当处理等距像素艺术时,RhysD\’s guide可以告诉你你想知道的一切。有些理论看维基百科比较好。

创建等距艺术时,一般的规则是:

  • 开始一个空白的等距网格,坚持完美像素精度。
  • 尝试打破单一等距tile图像艺术。
  • 尽量确保每个tile是允许通过或禁止通过。这将是复杂的,我们需要包含一个单一的tile同时包含可通行区域和非通行区域。大部分tile都需要一个或多个方向上的无缝tile.
  • 阴影的实现可能会非常棘手,除非我们使用分层的方法,我们将阴影绘制在底层,然会将英雄(或树木,或其他物体)绘制在顶层。如果你使用的不是分层的方法,当英雄站在树后面时,确保阴影落到前面,让它们不会重叠。
  • 假如你需要使用tile大于标准等距tile大小的图像时,请尝试使用多重tile标准尺寸。在这种情况下,最好是用分层的方法,根据其高度我们可以分割成不同的部分。例如,树可以被分成三个部分:根,树干,枝叶。这使得它更容易进行排序,我们可以根据高度将其绘制在相应的层。

大于标准tile尺寸的图像将是创建深度排序的障碍。在下面链接中对这个问题进行了一些讨论:

相关文章
Bigger tiles
Splitting and Painter’s algorithm
Openspace’s post on effective ways of splitting up larger tiles


5.等距角色

创建等距角色视图听起来并不复杂。角色艺术需要按照一定的标准创建。首先,我们需要确定在我们的游戏中有多少运动方向–通常是四方向运动或八方向运动。
iso-directions-600px.png

在一个自上而下的视图中,我们可以创建一组朝向一个方向的动画角色,只需要旋转就可以得到其他方向。

对于等距角色艺术,我们需要在每个允许的方向上重新显示每个动画–所以对于八方向运动,我们需要创建8个动画。为了便于理解,我们通常指的方向是:北,西北,西,西南,东南,东,东北,沿逆时针顺序。

spriteSheet.png

我们将以与tile相同的方式放置角色。角色的移动是通过在笛卡尔坐标系中计算运动,然后转换为等距坐标。假如我们是使用键盘控制人物。

我们将设置两个变量dX和dY,根据按下的方向键。默认情况下,这些变量为0,在下面的表格中 U , D , R 和 L分别表示上,下,左和右箭头键。值为1时表示该键被按下,为0时没有被按下。

  1. Key       Pos
  2. U D R L    dX dY
  3. ================
  4. 0 0 0 0     0  0
  5. 1 0 0 0     0  1
  6. 0 1 0 0     0 -1
  7. 0 0 1 0     1  0
  8. 0 0 0 1    -1  0
  9. 1 0 1 0     1  1
  10. 1 0 0 1    -1  1
  11. 0 1 1 0     1 -1
  12. 0 1 0 1    -1 -1

复制代码


现在,我们可以使用dX和dY的值,来更新笛卡尔坐标,像这样:

  1. newX = currentX (dX * speed);
  2. newY = currentY (dY * speed);

复制代码

因此,根据按下的键,dX和dY表示角色x,和y位置的变化。我们可以很容易的计算出新的等距坐标,正如我们已经讨论过的:

  1. Iso = twoDToIso(new Point(newX, newY))

复制代码

一旦我们有了新的等距位置,我们就要将角色移动到这个位置。根据dX和dY的值,我们可以确定角色面对的方向,并使用相应的角色艺术。

碰撞检测

碰撞检测是通过检查一个新位置上的tile是否为可通行tile.因此,一旦找到了新的位置,我们不立即移动角色,而是,首先检查tile是否可通行。

  1. tile coordinate = getTileCoordinates(isoTo2D(iso point), tile height);
  2. if (isWalkable(tile coordinate)) {
  3.   moveCharacter();
  4. } else {
  5.   //do nothing;
  6. }

复制代码

在isWalkable()函数中,我们检查数据数组值的坐标是否是一个可通行的tile.我们必须考虑角色朝向的方向–即使不动的情况下,也要检测tile是否可通行。

角色深度排序

考虑等距世界里的一个角色和一颗树。

为了正确理解深度排序,我们必须明白,当角色的x和y坐标小于树时,树覆盖角色,当角色坐标大于树时,角色覆盖树。


当他们具有相同的x坐标时,我们取决于单独的y坐标:具有较高的y坐标的会覆盖另一个,当有相同的y坐标时,取决于x坐标,具有较高的x坐标的会覆盖另一个.


一个简化版本就是从最远的地方开始绘制,即 tile[0][0],然后一行一个绘制tile。如果一个角色占用tile,我们首先绘制地面tile,然后绘制角色tile.这将正常工作,因为角色不会占用墙tile.

深度排序每次必须完成每一次tile位置的改变。举例来说,我们在角色移动后,更新显示场景,进行深度排序后,反映深度的变化。

6.试一试

现在,很好地利用你学到的新知识,来创造一个项目,用键盘控制,适当的深度排序和碰撞检测。这里是我的演示:

点击获得焦点,然后使用方向键。点击这里为全尺寸的版本

你可能会觉得这个工具类有用(我用AS3写的,但你应该能理解他能用于任何编程语言):

  1. package com.csharks.juwalbose
  2. {
  3.         import flash.display.Sprite;
  4.         import flash.geom.Point;
  5.         public class IsoHelper
  6.         {
  7.                 /**
  8.                  * convert an isometric point to 2D
  9.                  * */
  10.                 public static function isoTo2D(pt:Point):Point{
  11.                         //gx=(2*isoy isox)/2;
  12.                         //gy=(2*isoy-isox)/2
  13.                         var tempPt:Point=new Point(0,0);
  14.                         tempPt.x=(2*pt.y pt.x)/2;
  15.                         tempPt.y=(2*pt.y-pt.x)/2;
  16.                         return(tempPt);
  17.                 }
  18.                 /**
  19.                  * convert a 2d point to isometric
  20.                  * */
  21.                 public static function twoDToIso(pt:Point):Point{
  22.                         //gx=(isox-isoxy;
  23.                         //gy=(isoy isox)/2
  24.                         var tempPt:Point=new Point(0,0);
  25.                         tempPt.x=pt.x-pt.y;
  26.                         tempPt.y=(pt.x pt.y)/2;
  27.                         return(tempPt);
  28.                 }
  29.                 /**
  30.                  * convert a 2d point to specific tile row/column
  31.                  * */
  32.                 public static function getTileCoordinates(pt:Point, tileHeight:Number):Point{
  33.                         var tempPt:Point=new Point(0,0);
  34.                         tempPt.x=Math.floor(pt.x/tileHeight);
  35.                         tempPt.y=Math.floor(pt.y/tileHeight);
  36.                         return(tempPt);
  37.                 }
  38.                 /**
  39.                  * convert specific tile row/column to 2d point
  40.                  * */
  41.                 public static function get2dFromTileCoordinates(pt:Point, tileHeight:Number):Point{
  42.                         var tempPt:Point=new Point(0,0);
  43.                         tempPt.x=pt.x*tileHeight;
  44.                         tempPt.y=pt.y*tileHeight;
  45.                         return(tempPt);
  46.                 }
  47.         }
  48. }

复制代码

如果你卡在这,这里有完整的代码(在flash的时间轴上写的AS3代码)

  1. // Uses senocular\’s KeyObject class
  2. // http://www.senocular.com/flash/actionscript/?file=ActionScript_3.0/com/senocular/utils/KeyObject.as
  3. import flash.display.Sprite;
  4. import com.csharks.juwalbose.IsoHelper;
  5. import flash.display.MovieClip;
  6. import flash.geom.Point;
  7. import flash.filters.GlowFilter;
  8. import flash.events.Event;
  9. import com.senocular.utils.KeyObject;
  10. import flash.ui.Keyboard;
  11. import flash.display.Bitmap;
  12. import flash.display.BitmapData;
  13. import flash.geom.Matrix;
  14. import flash.geom.Rectangle;
  15. var levelData=[[1,1,1,1,1,1],
  16. [1,0,0,2,0,1],
  17. [1,0,1,0,0,1],
  18. [1,0,0,0,0,1],
  19. [1,0,0,0,0,1],
  20. [1,1,1,1,1,1]];
  21. var tileWidth:uint = 50;
  22. var borderOffsetY:uint = 70;
  23. var borderOffsetX:uint = 275;
  24. var facing:String = “south”;
  25. var currentFacing:String = “south”;
  26. var hero:MovieClip=new herotile();
  27. hero.clip.gotoAndStop(facing);
  28. var heroPointer:Sprite;
  29. var key:KeyObject = new KeyObject(stage);//Senocular KeyObject Class
  30. var heroHalfSize:uint=20;
  31. //the tiles
  32. var grassTile:MovieClip=new TileMc();
  33. grassTile.gotoAndStop(1);
  34. var wallTile:MovieClip=new TileMc();
  35. wallTile.gotoAndStop(2);
  36. //the canvas
  37. var bg:Bitmap = new Bitmap(new BitmapData(650,450));
  38. addChild(bg);
  39. var rect:Rectangle=bg.bitmapData.rect;
  40. //to handle depth
  41. var overlayContainer:Sprite=new Sprite();
  42. addChild(overlayContainer);
  43. //to handle direction movement
  44. var dX:Number = 0;
  45. var dY:Number = 0;
  46. var idle:Boolean = true;
  47. var speed:uint = 5;
  48. var heroCartPos:Point=new Point();
  49. var heroTile:Point=new Point();
  50. //add items to start level, add game loop
  51. function createLevel()
  52. {
  53.         var tileType:uint;
  54.         for (var i:uint=0; i<levelData.length; i )
  55.         {
  56.                 for (var j:uint=0; j<levelData[0].length; j )
  57.                 {
  58.                         tileType = levelData[i][j];
  59.                         placeTile(tileType,i,j);
  60.                         if (tileType == 2)
  61.                         {
  62.                                 levelData[i][j] = 0;
  63.                         }
  64.                 }
  65.         }
  66.         overlayContainer.addChild(heroPointer);
  67.         overlayContainer.alpha=0.5;
  68.         overlayContainer.scaleX=overlayContainer.scaleY=0.5;
  69.         overlayContainer.y=290;
  70.         overlayContainer.x=10;
  71.         depthSort();
  72.         addEventListener(Event.ENTER_FRAME,loop);
  73. }
  74. //place the tile based on coordinates
  75. function placeTile(id:uint,i:uint,j:uint)
  76. {
  77. var pos:Point=new Point();
  78.         if (id == 2)
  79.         {
  80.                 id = 0;
  81.                 pos.x = j * tileWidth;
  82.                 pos.y = i * tileWidth;
  83.                 pos = IsoHelper.twoDToIso(pos);
  84.                 hero.x = borderOffsetX pos.x;
  85.                 hero.y = borderOffsetY pos.y;
  86.                 //overlayContainer.addChild(hero);
  87.                 heroCartPos.x = j * tileWidth;
  88.                 heroCartPos.y = i * tileWidth;
  89.                 heroTile.x=j;
  90.                 heroTile.y=i;
  91.                 heroPointer=new herodot();
  92.                 heroPointer.x=heroCartPos.x;
  93.                 heroPointer.y=heroCartPos.y;
  94.         }
  95.         var tile:MovieClip=new cartTile();
  96.         tile.gotoAndStop(id 1);
  97.         tile.x = j * tileWidth;
  98.         tile.y = i * tileWidth;
  99.         overlayContainer.addChild(tile);
  100. }
  101. //the game loop
  102. function loop(e:Event)
  103. {
  104.         if (key.isDown(Keyboard.UP))
  105.         {
  106.                 dY = -1;
  107.         }
  108.         else if (key.isDown(Keyboard.DOWN))
  109.         {
  110.                 dY = 1;
  111.         }
  112.         else
  113.         {
  114.                 dY = 0;
  115.         }
  116.         if (key.isDown(Keyboard.RIGHT))
  117.         {
  118.                 dX = 1;
  119.                 if (dY == 0)
  120.                 {
  121.                         facing = “east”;
  122.                 }
  123.                 else if (dY==1)
  124.                 {
  125.                         facing = “southeast”;
  126.                         dX = dY=0.5;
  127.                 }
  128.                 else
  129.                 {
  130.                         facing = “northeast”;
  131.                         dX=0.5;
  132.                         dY=-0.5;
  133.                 }
  134.         }
  135.         else if (key.isDown(Keyboard.LEFT))
  136.         {
  137.                 dX = -1;
  138.                 if (dY == 0)
  139.                 {
  140.                         facing = “west”;
  141.                 }
  142.                 else if (dY==1)
  143.                 {
  144.                         facing = “southwest”;
  145.                         dY=0.5;
  146.                         dX=-0.5;
  147.                 }
  148.                 else
  149.                 {
  150.                         facing = “northwest”;
  151.                         dX = dY=-0.5;
  152.                 }
  153.         }
  154.         else
  155.         {
  156.                 dX = 0;
  157.                 if (dY == 0)
  158.                 {
  159.                         //facing=”west”;
  160.                 }
  161.                 else if (dY==1)
  162.                 {
  163.                         facing = “south”;
  164.                 }
  165.                 else
  166.                 {
  167.                         facing = “north”;
  168.                 }
  169.         }
  170.         if (dY == 0 && dX == 0)
  171.         {
  172.                 hero.clip.gotoAndStop(facing);
  173.                 idle = true;
  174.         }
  175.         else if (idle||currentFacing!=facing)
  176.         {
  177.                 idle = false;
  178.                 currentFacing = facing;
  179.                 hero.clip.gotoAndPlay(facing);
  180.         }
  181.         if (! idle && isWalkable())
  182.         {
  183.                 heroCartPos.x =  speed * dX;
  184.                 heroCartPos.y =  speed * dY;
  185.                 heroPointer.x=heroCartPos.x;
  186.                 heroPointer.y=heroCartPos.y;
  187.                 var newPos:Point = IsoHelper.twoDToIso(heroCartPos);
  188.                 //collision check
  189.                 hero.x = borderOffsetX newPos.x;
  190.                 hero.y = borderOffsetY newPos.y;
  191.                 heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth);
  192.                 depthSort();
  193.                 //trace(heroTile);
  194.         }
  195.         tileTxt.text=”Hero is on x: ” heroTile.x ” & y: ” heroTile.y;
  196. }
  197. //check for collision tile
  198. function isWalkable():Boolean{
  199.         var able:Boolean=true;
  200.         var newPos:Point =new Point();
  201.         newPos.x=heroCartPos.x   (speed * dX);
  202.         newPos.y=heroCartPos.y   (speed * dY);
  203.         switch (facing){
  204.                 case “north”:
  205.                         newPos.y-=heroHalfSize;
  206.                 break;
  207.                 case “south”:
  208.                         newPos.y =heroHalfSize;
  209.                 break;
  210.                 case “east”:
  211.                         newPos.x =heroHalfSize;
  212.                 break;
  213.                 case “west”:
  214.                         newPos.x-=heroHalfSize;
  215.                 break;
  216.                 case “northeast”:
  217.                         newPos.y-=heroHalfSize;
  218.                         newPos.x =heroHalfSize;
  219.                 break;
  220.                 case “southeast”:
  221.                         newPos.y =heroHalfSize;
  222.                         newPos.x =heroHalfSize;
  223.                 break;
  224.                 case “northwest”:
  225.                         newPos.y-=heroHalfSize;
  226.                         newPos.x-=heroHalfSize;
  227.                 break;
  228.                 case “southwest”:
  229.                         newPos.y =heroHalfSize;
  230.                         newPos.x-=heroHalfSize;
  231.                 break;
  232.         }
  233.         newPos=IsoHelper.getTileCoordinates(newPos,tileWidth);
  234.         if(levelData[newPos.y][newPos.x]==1){
  235.                 able=false;
  236.         }else{
  237.                 //trace(“new”,newPos);
  238.         }
  239.         return able;
  240. }
  241. //sort depth & draw to canvas
  242. function depthSort()
  243. {
  244.         bg.bitmapData.lock();
  245.         bg.bitmapData.fillRect(rect,0xffffff);
  246.         var tileType:uint;
  247.         var mat:Matrix=new Matrix();
  248.         var pos:Point=new Point();
  249.         for (var i:uint=0; i<levelData.length; i )
  250.         {
  251.                 for (var j:uint=0; j<levelData[0].length; j )
  252.                 {
  253.                         tileType = levelData[i][j];
  254.                         //placeTile(tileType,i,j);
  255.                         pos.x = j * tileWidth;
  256.                         pos.y = i * tileWidth;
  257.                         pos = IsoHelper.twoDToIso(pos);
  258.                         mat.tx = borderOffsetX pos.x;
  259.                         mat.ty = borderOffsetY pos.y;
  260.                         if(tileType==0){
  261.                                 bg.bitmapData.draw(grassTile,mat);
  262.                         }else{
  263.                                 bg.bitmapData.draw(wallTile,mat);
  264.                         }
  265.                         if(heroTile.x==j&&heroTile.y==i){
  266.                                 mat.tx=hero.x;
  267.                                 mat.ty=hero.y;
  268.                                 bg.bitmapData.draw(hero,mat);
  269.                         }
  270.                 }
  271.         }
  272.         bg.bitmapData.unlock();
  273. //add character rectangle
  274. }
  275. createLevel();

复制代码

注册点

特别注意tile和英雄的注册点。(注册点可以看做每个特定sprite的原点)。这一般不会在图像内部,而是在边框左上角。

我们将不得不改变我们的绘制代码来调整正确的注册点,主要用于英雄。

碰撞检测

另一点要注意的是,我们计算碰撞检测是基于英雄所在的点。

但英雄具有体积,不可以准确的表示为一个单一的点,所以我们需要用一个矩形来代表英雄并检查这个矩形和每个角落的碰撞,所以,才没有和其他tile和深度的对象重叠。

捷径

在演示中,当英雄位置更新时,我只是重绘场景的每一帧。我们发现当遍历其他tile时,英雄tile会在地面tile前面。

但是,如果我们仔细看就会发现,这种情况下没必要遍历所有的tile.草tile和墙tile的顶部和左侧都先于英雄绘制,所以我们不需要重绘它们。另外,底部和右侧的tile总是在英雄的前面,所以英雄是后绘制的。

从本质上讲,我们只需要对墙内活动区域和英雄进行深度排序-也就是两个tile.注意到这些捷径将帮助你节省大量的处理时间,可能是性能的关键。

总结
现在,建立一个自己的等距游戏,你应该有了很好的基础:你可以创建世界和对象,用简单的二维数组表示水平数据,转换笛卡尔坐标和等距坐标,和处理的概念,如深度排序和角色动画。希望你喜欢建造等距世界。

原文链接:http://gamedev.tutsplus.com/tutorials/implementation/creating-isometric-worlds-a-primer-for-game-developers/

 

http://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-a-primer-for-gamedevs-continued- -gamedev-9215

弄懂导向行为-逃离和抵达

原文链接:http://gamedevelopment.tutsplus.com/tutorials/understanding-steering-behaviors-flee-and-arrival–gamedev-1303

跑开

在前面介绍的寻找行为中,我们介绍了一种基于两个向量力来将人物推向目标的行为方式:预期速度和控制力

desired_velocity = normalize(target – position) * max_velocity ;

steering = desired_velocity – velocity ;

在上面说的desired_velocity是从人物直接指向目标的。我们只要简单的将目标点减去人物的点就能够得到这个速度。

而在本节中,同样的,我们也需要使用这样的两个力,但是,我们却是使用这两个力来避免物体移动到目标那里去。

我们现在计算新的desired_velocity,这个速度想在用人物的位置减去目标的位置,这样得到的desired_velocity方向就和上面介绍的行为方式的方向是相反的

我们同样的来用代码来阐释这个概念:

desired_velocity = normalize(position – target) * max_velocity

steering = desired_velocity – velocity

上面计算出来的desired_velocity表示了最容易逃离的路线。而产生的steering控制力将会使的物体丢弃原有的移动路径,而迫使它向desired_velocity靠近。

我们可以将逃离行为和寻找行为联系在一起:

flee_desired_velocity = -seek_desired_velocity

换句话说,一个向量是另外一个向量的反向向量。

增加逃离力

当我们计算出来了逃离的力之后,我们就必须将这个力添加到人物的速度向量上面去。由于这个力是将物体推向目标相反的方向上去,所以,每一帧物体都会向逃离目标的方向去移动,自然而然就产生了一条逃离路径了。

如何增加逃离力,和前面介绍的seek行为是一样的累积方法。

上面的算法是一直的逃离,我们可以添加一个熟悉来控制,如果距离目标有一段距离之后就不再进行逃离。

抵达

寻找行为使的物体向一个指定的目标移动。当到达这个目标之后,物体会在目标附近来回不断的乱动,也就是说会穿过目标地点。而抵达行为就是控制人物不要穿过这个目标点,在当人物慢慢的接近目标点的时候,速度会慢慢的下降,直到最后在目的地点停止下来。

这个行为由两个部分控制。当人物远离目标地点的时候,就像原来的seek行为一样。

当人物距离目标地点很近的时候,在减速区域的时候,我们就慢慢的让人物减速下来:

当人物进入这个圆圈的时候,它就会慢慢的减速下来。

减速

当人物进入了减速区域的时候,它的速度就会线性的慢慢的减下来。这样的效果可以通过简单的增加一个新的控制力到人物的速度向量上来就可以做到。当这个方法最终增加的速度变成0的时候,那么就表示将没有任何的速度添加到这个人物的位置上:

  1. “font-family:Microsoft YaHei;”>// If (velocity   steering) equals zero, then there is no movement
  2. velocity = truncate(velocity   steering, max_speed)
  3. position = position   velocity
  4. function truncate(vector:Vector3D, max:Number) :void {
  5.     var i :Number;
  6.     i = max / vector.length;
  7.     i = i < 1.0 ? i : 1.0;
  8.     vector.scaleBy(i);
  9. }

为了保证人物会慢慢的减速下来,我们不能立即的将人物的速度变成0.我们可以通过人物现在距离目标的地点和减速区域的半径来判断应该对人物现有的速度做多少的变化,从而能够很平滑的实现减速效果:

  1. “font-family:Microsoft YaHei;”>// Calculate the desired velocity
  2. desired_velocity = position – target
  3. distance = length(desired_velocity)
  4. // Check the distance to detect whether the character
  5. // is inside the slowing area
  6. if (distance < slowingRadius) {
  7.     // Inside the slowing area
  8.     desired_velocity = normalize(desired_velocity) * max_velocity * (distance / slowingRadius)
  9. else {
  10.     // Outside the slowing area.
  11.     desired_velocity = normalize(desired_velocity) * max_velocity
  12. }
  13. // Set the steering based on this
  14. steering = desired_velocity – velocity

如果人物距离目标地点的距离远远的大于slowRadious,那么就意味着这个人物距离目标地点非常的远,所以,我们只要简单的使用寻找行为来寻找目标点即可,并且保持速度不变。

如果他们之间的距离比slowRadious要小的时候,这就意味着,人物抵达了减速区域,所以现在我们需要将它进行减速。

distanc/slowRadious 的值在减速区域中将会从0到1不等。这样的线性变化就会使得人物的移动速度慢慢的呈线性减速下来。

正如前面介绍的那样,这个移动方式将会像下面这样进行表示:

  1. “font-family:Microsoft YaHei;”>steering = desired_velocity – velocity
  2. velocity = truncate (velocity   steering , max_speed)
  3. position = position   velocity

如果预期速度最后降低到0的时候,那么控制力的大小就会变成-velocity。所以,把这个行为力加到速度上的时候,就会导致人物最终移动的速度变成了0,停止下来了。

结论

逃离行为使的人物远离目标地点,而抵达行为使得人物在抵达地点时停止下来。这样行为都是可以在一定程度上进行组合从而做成类似于跟随,逃离这样的行为。

弄到导向行文:seek

原文地址链接:http://gamedevelopment.tutsplus.com/tutorials/understanding-steering-behaviors-seek–gamedev-849

位置,速度和移动

在行为控制中的所有的算法实现都是通过数学上的向量计算来实现的。由于这个控制会改变人物的速度和位置,所以同样的我们也可以使用向量来表示这些属性。

虽然向量拥有一个方向,但是当用向量来表示一个位置的时候,我们往往可以忽略它的方向:

上面的图中P向量表示的是一个点(x,y),V向量表示的是一个速度(a,b),我们可以通过欧拉积分的方法来计算人物新的位置:

position = position velocity ;

速度向量的方向控制了一个人物将要向哪个方向移动,速度向量的大小控制了人物每一帧将会移动多少距离。这个长度越大,那么人物每一帧移动的距离就会越大。我们可以通过速度向量的长度来确保,速度的大小不会高于某个指定的值。

我们可以通过施加力来实现seek行为,也可以不施加力,仅仅施加一个改变速度,也能够模拟出来。比如如下的代码阐述了在不施加力的情况下如何进行seek行为:

velocity = normalize(target – position) * max_velocity ;

我们需要注意的是,如果不使用施加力的方式,那么一旦seek的目标发生了变化,人物的速度也运动方向将会在瞬间发生改变,所以就会变得不是那么的圆滑。

计算力

如果只有速度方向上的力参与改变的话,那么人物角色将会沿着一条直线进行运动。所以,我们可以通过增加一个力来改变人物的移动,不同的力将会导致物体移动向不同的方向上去。

对于seek行为来说,每一帧增加一点控制力将会很平滑的改变人物移动的速度和方向,避免出现瞬间改变方向的问题。当目标发生移动的时候,施加力的方案也能够很好的平滑的改变速度,使得人物到达目标点。

seek行为需要如下的两种向量:期望速度和控制力:

上图中的desired velocity就是最终人物要到达的速度方向。而steering就是需要增加到人物上面的行为控制力,通过这个控制力,就会将人物推向了目标地点。

这些力可以通过如下的算法求得:

desired_velocity = normalize(target – position) * max_velocity ;

steering = desired_velocity – velocity ;

增加力

当控制力计算出来之后,我们就需要将这个力添加到人物中去。每一帧我们都需要计算新的steering控制力,这样才能够保证每一帧物体都是朝着目标的方向前进的。下图中可以看出,我们的seek路径并不是一条直线,而是很平滑的一条曲线:

下面的代码表示了如何增加力:

steering = truncate(steering, max_force);

steering = steering / mass ;

velocity = truncate( velocity steering, max_speed);

position = position velocity ;

上面的计算方式保证了,我们添加的力在可允许的范围之内。我们还将这个力除以了人物的质量,这样就会产生不同质量的物体,他们改变的频率并不一致,所以这样就会看上去更加的丰富了。

结论

行为控制能够产生很多不同的,比较真实的移动效果。我们很容易的就能够通过当前的信息计算出新的控制行为出来。即使计算的方法非常的简单,但是,我们依然能够创建出很多比较有趣和复杂的运动方法出来。

弄懂导向行为:碰撞躲避

原文链接:http://gamedevelopment.tutsplus.com/tutorials/understanding-steering-behaviors-collision-avoidance–gamedev-7777

介绍

进行碰撞躲避的方法实际上非常的简单,就是在我们检测到“将要”和一个碰撞物相撞的时候,我们产生一个躲避的力来避免进行碰撞。记住,我们仅仅使用一个最近的障碍物来计算躲避的力,就算有很多的障碍物,这样的算法同样能够很好的运行。

所以可以看出,这个算法就需要解决两个问题:如何确定最近的物体,以及如何进行躲避力的产生。

读者需要注意,我们这里阐述的碰撞躲避算法并不是使用一种寻路算法来避免碰撞到障碍物。这种算法对于比较复杂的环境可能不是很适用,但是对于相对比较简单的环境来说,这种算法效果更加的出色。

朝向向量

进行碰撞躲避的首要问题就是预先处理碰撞。我们唯一需要躲避的障碍物就是距离我们的人物最近的障碍物。我们首先产生一个称为ahead的向量,这个向量实际上和我们的速度向量方向一致,所以我称之为朝向向量,但是他们的长度并不是相同的:

我们使用如下的方法来计算这个ahead向量的位置:

ahead = position normlize(velocity) * MAX_SEE_AHEAD ;

这个ahead向量的长度定义了它在距离障碍物多远的时候就能够进行碰撞躲避。MAX_SEE_HEAD越大,那么角色反应的时间就越早,就越有可能躲避成功。但是同样的,如果太大了,那么障碍物在距离角色很远的时候就进行躲避了,所以我们需要定义合适的值:

碰撞检测

为了能够进行碰撞检测,我们这里将所有的障碍物都假设为圆形的。使用圆形的障碍物,效果非常的好,也容易实现。

为了进行碰撞检测,我们需要对使用直线-圆相交的碰撞检测,但是在这里我会使用一个更加简单有效的方法。

我们使用ahead向量再次的创建一个向量ahead2,这个向量和ahead向量方向一致,只是长度只有它的一半:

我们使用下面的方法来计算这个ahead2向量:

ahead = position normlaize(velocity) * MAX_SEE_AHEAD ;

ahead2 = position normlaize(velocity) * MAX_SEE_AHEAD * 0.5 ;

进行碰撞检测,我们只需要判断这两个点是否在障碍物所构成的圆形中就可以。

如果他们之间的距离小于或者等于圆形的半径,那么我们就认为这个碰撞发生了:

当d < r的时候,我们就认为预先碰撞发生了。

如果这两个点都不在原型中,那么就认为没有发生碰撞,下面是进行碰撞的代码:

  1. private function distance(a :Object, b :Object) :Number {
  2.     return Math.sqrt((a.x – b.x) * (a.x – b.x)    (a.y – b.y) * (a.y – b.y));
  3. }
  4. private function lineIntersectsCircle(ahead :Vector3D, ahead2 :Vector3D, obstacle :Circle) :Boolean {
  5.     // the property “center” of the obstacle is a Vector3D.
  6.     return distance(obstacle.center, ahead) <= obstacle.radius || distance(obstacle.center, ahead2) <= obstacle.radius;
  7. }

如果有多个物体挡住了路,那么我们只对最近的一个障碍物进行处理:

计算躲避力

躲避力必须要将角色推开,这样才能够使角色躲避掉障碍物。我们可以用如下的代码来计算躲避力:

avoidance_force = ahead - obstacle_center
avoidance_force = normalize(avoidance_force) * MAX_AVOID_FORCE

当我们有了躲避力之后,我们就用MAX_AVOID_FORCE来对躲避力进行缩放,这样就能够得到某个方向上特定大小的躲避力了。

躲避障碍物

下面是整个collisionAvoidance的实现,这个函数用来产生一个躲避力:

  1. private function collisionAvoidance() :Vector3D {
  2.     ahead = …; // calculate the ahead vector
  3.     ahead2 = …; // calculate the ahead2 vector
  4.     var mostThreatening :Obstacle = findMostThreateningObstacle();
  5.     var avoidance :Vector3D = new Vector3D(0, 0, 0);
  6.     if (mostThreatening != null) {
  7.         avoidance.x = ahead.x – mostThreatening.center.x;
  8.         avoidance.y = ahead.y – mostThreatening.center.y;
  9.         avoidance.normalize();
  10.         avoidance.scaleBy(MAX_AVOID_FORCE);
  11.     } else {
  12.         avoidance.scaleBy(0); // nullify the avoidance force
  13.     }
  14.     return avoidance;
  15. }
  16. private function findMostThreateningObstacle() :Obstacle {
  17.     var mostThreatening :Obstacle = null;
  18.     for (var i:int = 0; i < Game.instance.obstacles.length; i ) {
  19.         var obstacle :Obstacle = Game.instance.obstacles[i];
  20.         var collision :Boolean = lineIntersecsCircle(ahead, ahead2, obstacle);
  21.         // “position” is the character\’s current position
  22.         if (collision && (mostThreatening == null || distance(position, obstacle) < distance(position, mostThreatening))) {
  23.             mostThreatening = obstacle;
  24.         }
  25.     }
  26.     return mostThreatening;
  27. }

躲避力必须对物体的速度产生影响,如果是基于力的系统,我们就简单的将力积蓄就可以。

  1. steering = nothing(); // the null vector, meaning “zero force magnitude”
  2. steering = steering   seek(); // assuming the character is seeking something
  3. steering = steering   collisionAvoidance();
  4. steering = truncate (steering, max_force)
  5. steering = steering / mass
  6. velocity = truncate (velocity   steering, max_speed)
  7. position = position   velocity

由于每一次更新,我们都重新的计算躲避力,所以只要还在躲避的范围,就会一直受到这个躲避力的作用,从而躲避开来。

如果ahead向量没有与障碍物发生碰撞,那么就不需要进行躲避了。

Oops,博主自己的话语

上面的东西,只是简单对原文进行了翻译。为了很好的理解这个概念,我自己做了一个Demo。读者需要注意,上面的方法是基于力的,如果你的项目并不是基于力(和博主自己的Demo一样),而只是简单的使用速度的话,同样也可以实现。下面我用cocos2d-x来实现一个Demo。

首先,我创建四个障碍物,如下所示:

  1. //Create 4 obstacles
  2. for(int i = 0 ; i < 4 ; i  )
  3. {
  4.   float x = 100 ;
  5.   float y = 100 ;
  6.   if(i >= 2)
  7.     x = 380 ;
  8.   if(i == 1 || i == 3)
  9.         y = 220 ;
  10.   m_Obstacals[i] = CCSprite::create(“Obstacle.png”);
  11.   m_Obstacals[i]->retain();
  12.   m_Obstacals[i]->setPosition(ccp( x, y ));
  13.   this->addChild(m_Obstacals[i]);
  14. }

然后创建一个角色:

  1. //Create the character
  2.  m_Character = CCSprite::create(“Character.png”);
  3.  m_Character->retain();
  4.  m_Character->setPosition(ccp(0,0));
  5.  this->addChild(m_Character);
  6.  m_velocity = ccp(2,4);

这个很简单,只是在窗口中贴上几张图而已。

下面是场景的Update方法,主要的任务都是在这里进行的:

  1. void HelloWorld::update(float dt)
  2. {
  3.     CCPoint pos = m_Character->getPosition();
  4.     pos = ccpAdd(pos, m_velocity);
  5.     m_Character->setPosition(pos);
  6.     if(pos.x < 0 || pos.x > 480)
  7.         m_velocity.x =- m_velocity.x ;
  8.     if(pos.y < 0 || pos.y > 320)
  9.         m_velocity.y = – m_velocity.y ;
  10.     //Collision avoidance
  11.     //————————————————
  12.     //Step 1: Find the most closest obstacal
  13.     CCPoint c_pos = m_Character->getPosition();
  14.     float length = FLT_MAX ;
  15.     int  index = -1 ;
  16.     for(int i = 0 ; i < 4 ; i  )
  17.     {
  18.         CCPoint temp_pos = m_Obstacals[i]->getPosition();
  19.         temp_pos = ccpSub(temp_pos, c_pos);
  20.         float temp_length = ccpLength(temp_pos);
  21.         if(temp_length < length)
  22.         {
  23.             length = temp_length ;
  24.             index = i ;
  25.         }
  26.     }// end for
  27.     //Step2: Check for collision
  28.     CCPoint ob_pos = m_Obstacals[index]->getPosition();
  29.     float ob_radious = m_Obstacals[index]->getContentSize().width/2 ;
  30.     float dynamic_velocity = MAX_SEE_VALUE ;
  31.     CCPoint ahead = ccpAdd(c_pos ,cocos2d::ccpMult(ccpNormalize(m_velocity), dynamic_velocity));
  32.     CCPoint ahead2 = ccpAdd(c_pos ,cocos2d::ccpMult(ccpNormalize(m_velocity), dynamic_velocity * 0.5));
  33.     if(ccpDistance(ahead, ob_pos) > ob_radious &&
  34.         ccpDistance(ahead2, ob_pos) > ob_radious &&
  35.         ccpDistance(c_pos, ob_pos) > ob_radious)
  36.         return ;
  37.     //Step3 : Do avoidance
  38.     CCPoint nor_velocity = ccpNormalize(m_velocity);
  39.     CCPoint toCenter = ccpSub(ob_pos, c_pos);
  40.     toCenter = ccpNormalize(toCenter);
  41.     CCPoint avoidance_velocity = ccp(nor_velocity.y, -nor_velocity.x);
  42.     if(ccpDot(toCenter, avoidance_velocity) > 0)
  43.     {
  44.         avoidance_velocity = ccp(-nor_velocity.y, nor_velocity.x);
  45.     }// end if
  46.     m_velocity = ccpAdd(m_velocity, ccpMult(avoidance_velocity, MAX_AVOIDANCE_SPEED));
  47.     if(ccpLength(m_velocity) > MAX_SPEED)
  48.     {
  49.         m_velocity = ccpMult(ccpNormalize(m_velocity), MAX_SPEED);
  50.     }
  51. }

这个函数很简单,首先我们使用速度来对角色的位置进行更新。

然后进行边界检查,判断是否出了屏幕区域,如果是,就将速度取反。

下面的就是进行碰撞躲避的算法了。

第一步,找到最近的障碍物,这个很简单,只要遍历所有的障碍物列表,然后计算最近的障碍物即可。

第二步,判断是否进行了碰撞。这个就和原文中使用的算法一致,加上判断角色是否已经在障碍物里面的条件。

第三部,进行躲避,由于不是基于力的系统,所以我就简单的计算一个躲避速度,然后把这个速度加到物体原有的速度上即可。但是注意,我这里计算躲避速度的方法与原文不一样。原文是使用ahead – center的方式来计算一个躲避力。这种方法,在物体不是直接朝着障碍物移动的时候,有效。但是,当角色直接的相障碍物移动的时候,你会发现这样计算出来的力的方向与物体移动的方向刚好相反,也就是说,会将物体向后推走,这样的方法,在屏幕显示上感觉会很怪,不是很平滑。所以我使用垂直于角色速度方向的方向作为躲避力的方向,这样不管是不是直接朝着障碍物中心移动,我们都能够平滑的躲避。

这里需要注意,垂直于速度方向上的向量有两个,他们互相相反,如何选取哪一个向量了?我们来看下面的图:

我们从上图中可以看出,有normal1和normal2两个向量,很明显,我们要旋转normal1,但是为什么了?因为它更容易使物体进行躲避。也就是说,我们只要能够判断“更容易是物体进行躲避”的条件就可以了。

这个条件就是将normal向量与toCenter向量进行点积操作,如果值小于0那么就是我们想要的那个向量,如果大于0就不是我们想要的。如果是0的话,那么随便哪一个都可以,因为这时物体是直接向障碍物中心移动的。

好了,就到这里了。下面是这个Demo的截图:

完整源代码请到这里下载:

 Source.zip

角度,弧度.周长.以及圆上的点

角度就不用多讲了.

弧度.弧长为半径时的度量单位称为1弧度,.即两条射线圆心向圆周射出,形成一个夹角和夹角正对的一段弧。当这段弧长正好等于圆的半径时,两条射线的夹角的弧度为1

弧度是一种特殊的角度

一周360度,周长是2πr(r即弧度对应的弧长).则360°角=2π*弧度.则1弧度约为57.3°,即57°17’44.806”..同样推算出1角度约为1/57.3=0.01745弧度

弧度既不是角度,也不是弧长..1弧度就是对应的弧长为半径,其角度为57.3°

 

Mathf.Sin 计算并返回以弧度为单位指定的角 f 的正弦值。

 

//  圆点坐标:(orignX,orignY)
//  半径:r
//  角度:angle
//         
//  则圆上任一点为:(x1,y1)
var x1 = orignX + r * Mathf.Cos(angle * Mathf.Deg2Rad );
var y1 = orignY + r * Mathf.Sin(angle * Mathf.Deg2Rad );