[翻译]XNA外文博客文章精选之twelve
PS:自己翻译的,转载请著明出处
RTS风格的移动:转向并向一个目标移动
Phantom
在许多游戏里都会有一个共同的特点,它的RTS(即时战略),PRG(角色扮演)或者其他游戏类型,是使精灵旋转后面对一个目标;在RTS里这通常是一个敌人或者正好是要移动到的一个位置。XNA使它变的十分简单去旋转精灵或者单独的或者做为一组旋转,但是找到角度可以稍微更复杂使用三角法和弧度。
这篇文章将会展示你如何去创建一个unit(个体),当给定一个地方旋转去面对它并且移动到那个位置。它同样会解释在后台的这个三角法和矩阵。
这个unit它本身将会可拖拉的游戏组件部分,这主要是由于IM只有创造一个unit在这个例子中。
1 public class Unit:DrawableGameComponent
2 {
3 private Texture2D m_unit;
4 private SpriteBatch m_batch;
5 private Matrix m_matrix;
6 private float m_movementspeed;
7 private float m_turningspeed;
这前面的一小片代码中,我们为这个unit包括5个基础的变量,它们都十分简单,这个unit的纹理,sprite batch去绘制它,这个矩阵,它将在后面被使用并且最后连个浮点型数去控制转向和unit的移动的速度。2 {
3 private Texture2D m_unit;
4 private SpriteBatch m_batch;
5 private Matrix m_matrix;
6 private float m_movementspeed;
7 private float m_turningspeed;
我们同样需要有一个变量为unit的当前的位置和旋转,但同样一个变量去保存位置,这个位置是我们象要精灵最终移动到的并且角度是面对这个位置。
1 private Vector2 m_endposistion;
2 public Vector2 EndPosition
3 {
4 get { return m_endposistion; }
5 set
6 {
7 m_endposistion = value;
8 UpdateEndDirection();
9 }
10 }
11
12 private Vector2 m_position;
13 private float m_enddirection;
14 private float m_rotation;
15 private float m_direction
16 {
17 get { return m_rotation; }
18 set
19 {
20 m_rotation = value;
21 if (m_rotation < 0)
22 {
23 m_rotation += (float)(Math.PI * 2);
24 }
25 else if (m_rotation > Math.PI * 2)
26 {
27 m_rotation -= (float)(Math.PI * 2);
28 }
29 }
30 }
这个m_endposition变量同样有一个公有属性作为当你希望发送这个unit到一个新的位置,每一次这个EndPosition被设置一个被调用的方法同样去更新m_enddirection变量。m_position保存这个units当前的位置和m_rotation保存当前的units的旋转,然而,这是通过m_direction属性被更新的,这个属性执行一个检查每一次它的更新去确保浮点值总是保持在0和2Pi之间;这是因为所有的C#使用弧度去测量在一个圆里的角度,一个整圆是2Pi的弧度,它近似于6.283,大于它或者小于也是可以使用的,但是产生了更多的麻烦,一会在计算上去结实这个数据。2 public Vector2 EndPosition
3 {
4 get { return m_endposistion; }
5 set
6 {
7 m_endposistion = value;
8 UpdateEndDirection();
9 }
10 }
11
12 private Vector2 m_position;
13 private float m_enddirection;
14 private float m_rotation;
15 private float m_direction
16 {
17 get { return m_rotation; }
18 set
19 {
20 m_rotation = value;
21 if (m_rotation < 0)
22 {
23 m_rotation += (float)(Math.PI * 2);
24 }
25 else if (m_rotation > Math.PI * 2)
26 {
27 m_rotation -= (float)(Math.PI * 2);
28 }
29 }
30 }
1 private Vector2 m_origin;
最后一个变量是初始点,这点是unit将会转向的,如果你想要unit开始在中心,因为是我选择要这样,然后设置你的unit的高度和宽度的一半。下一部分是结构器和加载内容,这都是非常直接的。因为这个unit是一个可拖拽的游戏组件部分,,我们必须传入到游戏对象中,但是我们同样传入unit的开始的位置。
1 public Unit(Game game, Vector2 pos): base (game)
2 {
3 m_position = pos;
4 m_endposistion = pos;
5 m_origin = new Vector2(32, 32;
6 m_movementspeed = 15f;
7 m_turningspeed = 0.05f;
8 }
9 protected override void LoadContent()
10 {
11 m_unit = Game.Content.Load<Texture2D>("Unit");
12 m_batch = new SpriteBatch(GraphicsDevice);
13 base.LoadContent();
14 }
开始的位置传入到构造器中,我们设置units当前和结束的位置,这是为了让unit开始不动。这个构造器同样设置我们初始位置和为了速度的两个浮点型。2 {
3 m_position = pos;
4 m_endposistion = pos;
5 m_origin = new Vector2(32, 32;
6 m_movementspeed = 15f;
7 m_turningspeed = 0.05f;
8 }
9 protected override void LoadContent()
10 {
11 m_unit = Game.Content.Load<Texture2D>("Unit");
12 m_batch = new SpriteBatch(GraphicsDevice);
13 base.LoadContent();
14 }
这LoadContent方法是十分简单的,它加载我们的unit纹理和使用spritebatch去绘制它。
1 public override void Draw(GameTime gameTime)
2 {
3 m_batch.Begin();
4 m_batch.Draw(m_unit, m_position, null, Color.Red, m_direction, m_origin, 1f, SpriteEffects.None, 0.0f);
5 m_batch.End();
6 base.Draw(gameTime);
7 }
Draw比平常略微不同,因为当我们调用draw方法在sprite batch上,我们使用第6个,这样我们可以传入m_direction作为第5个变量,这个变量绘制精灵面对的方向;我们同样传入我们的初始位置告诉它,旋转精灵去指向它。另外一个变量十分简单可以自我解释。2 {
3 m_batch.Begin();
4 m_batch.Draw(m_unit, m_position, null, Color.Red, m_direction, m_origin, 1f, SpriteEffects.None, 0.0f);
5 m_batch.End();
6 base.Draw(gameTime);
7 }
纹理,位置,没有源矩形,风格,当前旋转,在旋转点,缩放,没有效果,没有深度。
我喜欢保留update方法,简单放置大量代码在另外两个方法中。
1 public override void Update(GameTime gameTime)
2 {
3 if (m_direction != m_enddirection)
4 {// If your not currently facing the right direction turn
5 TurnTo();
6 }
7 else if (m_position != m_endposistion)
8 {// Else If your not at your end destination move
9 MoveTo();
10 }
11 base.Update(gameTime);
12 }
当update方法被调用,它首先检查当前的unit是否面对正确的方向,通过m_direction和m_enddirection的比较,如果它不是的,它调用TurnTo方法;如果unit当前是面对正如的方向,然后它把当前的位置(m_position)与结束的位置(m_endposition)相比较,如果unit不在结束的位置上它调用MoveTo方法。2 {
3 if (m_direction != m_enddirection)
4 {// If your not currently facing the right direction turn
5 TurnTo();
6 }
7 else if (m_position != m_endposistion)
8 {// Else If your not at your end destination move
9 MoveTo();
10 }
11 base.Update(gameTime);
12 }
MoveTo方法被用来更新unit的位置,而这正是更复杂的观念在起作用。首先的事情是这个方法的是得到当前和结束位置之间的距离,通过使用Pythagoreantheorem,幸运的是XNA有一个方法处理这个,调用Length方法在一个Vector2上,将给你提供两个角之间的直线距离。通过在两个位置之间你得到的Vector2的当前位置,减去结束点位置,并且调用Length方法在你提供的距离的一个浮点型上。
1 private void MoveTo()
2 {
3 float distance = (m_position - m_endposistion).Length();
4 if (distance < m_movementspeed)
5 {
6 m_position = m_endposistion;
7 }
8 else
9 {
10 m_matrix = Matrix.CreateTranslation(-m_position.X, -m_position.Y, 0);
11 m_matrix = Matrix.CreateRotationZ(-m_direction);
12 m_matrix *= Matrix.CreateTranslation(0,-m_movementspeed, 0);
13 m_matrix *= Matrix.CreateRotationZ(m_direction);
14 m_matrix *= Matrix.CreateTranslation(m_position.X, m_position.Y, 0);
一旦你有m_position和m_endposition之间的距离,首先检查的是unit是否达到结束的位置,这将通过m_movmentspeed是否比这个距离小来返回,如果是这种情况,没有做任何更多的计算你点只需要设置m_position与m_endposition相等。2 {
3 float distance = (m_position - m_endposistion).Length();
4 if (distance < m_movementspeed)
5 {
6 m_position = m_endposistion;
7 }
8 else
9 {
10 m_matrix = Matrix.CreateTranslation(-m_position.X, -m_position.Y, 0);
11 m_matrix = Matrix.CreateRotationZ(-m_direction);
12 m_matrix *= Matrix.CreateTranslation(0,-m_movementspeed, 0);
13 m_matrix *= Matrix.CreateRotationZ(m_direction);
14 m_matrix *= Matrix.CreateTranslation(m_position.X, m_position.Y, 0);
在这种情况下,unit不会达到结束位置,然后你需要unit以当前速度朝着这个结束位置移动。我选择使用一个矩阵去做这个,但是我同样还是要去解释这个方法,使用Pythagoreantheorem(勾股定理)
使用这个Matrix方法,这里有5步在你可以得到unit的新的向量位置之前。你将会看见每一个阶段的显示出的效果,因为它可以影响你的unit在每一个阶段。
1.开始的位置。
2.矩阵总是旋转在0,0上,这样你需要使用你当前位置的负方向转变这个矩阵
3.然后你需要旋转矩阵,这样它很容易去添加你的移动,所以如果你旋转它到你当前方向的负方向时,他将会指向上。
4.一旦矩阵被旋转,你现在刚好可以通过准确的速度来转换它,你希望你的unit通过这个速度来传播。
5.现在你有效的移动你的unit,如你所希望的,并且现在可以再运用于旋转。
6.最后再运用你前面的位置的平移,并且以你最终的位置结束。
1 Vector3 dest = m_matrix.Translation;
2 m_position.X = dest.X;
3 m_position.Y = dest.Y;
一旦你操作你的矩阵去产生渴望的向量,然后你可以使用这个矩阵的Translation属性去找到移动的总和,你创建并设置你的units位置到它们中。2 m_position.X = dest.X;
3 m_position.Y = dest.Y;
aulternative方法是用来找到两个位置之间的距离,并且使用移动速度除以它,这将产生更新需要得到结束位置的数量。一旦你有它,通过更新所需要的数量,你可以划分Vector2代表在这两个位置之间的距离,这将产生一个向量1更新了移动的值,并且所有你需要去做的是这个units的当前的位置。(译者:这是一位俄罗斯的人写的,十分难懂,可以参照原文看。)
1 Vector2 distance = m_position - m_endposistion;
2 float turns = distance.Length() / m_movementspeed;
3 distance /= turns;
4 m_position -= distance;
当unit不是面对正确的方向,TurnTo被调用,这个方法更新units的方向。方法的第一部分是有点象MoveTo方法,首先它得到在m_direction和m_enddirection之间的距离,然后它测试去看看这是否较小,然后改变速度,但是如果这个距离是一个负的,它添加一个"-"在这个距离的前面让它变成正的。
2 float turns = distance.Length() / m_movementspeed;
3 distance /= turns;
4 m_position -= distance;
1 private void TurnTo()
2 {
3 float d = m_enddirection - m_direction
4 if (d < 0 && -d < m_turningspeed || d > 0 && d < m_turningspeed)
5 {
6 m_direction = m_enddirection;// Turn to the end
7 }
8 else
9 {
10 if (d > 0 && d > Math.PI)
11 {
12 m_direction -= m_turningspeed;
13 }
14 else if (d < 0 && (-d < Math.PI))
15 {
16 m_direction -= m_turningspeed;
17 }
18 else
19 {
20 m_direction += m_turningspeed;
21 }
22 }
23 }
方法的第二部分通过m_turnignspeed给定的数量更新当前的方向,但是它同样决定是否转向左或者右。这两个IF语句决定这个,如果这个距离是一个正数并且比Pi大,或者如果这个距离是个负数并且比Pi小,unit需要转向左因为这是短距离的左转;否则unit需要转向右;2 {
3 float d = m_enddirection - m_direction
4 if (d < 0 && -d < m_turningspeed || d > 0 && d < m_turningspeed)
5 {
6 m_direction = m_enddirection;// Turn to the end
7 }
8 else
9 {
10 if (d > 0 && d > Math.PI)
11 {
12 m_direction -= m_turningspeed;
13 }
14 else if (d < 0 && (-d < Math.PI))
15 {
16 m_direction -= m_turningspeed;
17 }
18 else
19 {
20 m_direction += m_turningspeed;
21 }
22 }
23 }
最后的方法是UpdateEndDirection,它每一次被调用一个新的结束位置,这个位置被给予这个unit,它同样是支持三角法的方法。
为了得到结束的方法,你使用向量的X和Y坐标创建在当前和结束位置之间。该计算公式是:ATan(O/A)
ATan是一个数学函数提供在C#中,这个O部分代表从你想要找到的角度的直角三角型的对边,这个A部分代表邻边。我选择去使用X作为邻边和Y作为对边,你可以从相反的方向转动它,但是其他的计算部分稍后也必须改变。
1 private void UpdateEndDirection()
2 {
3 Vector2 d = m_endposistion - m_position;
4 double a = 0;
5 float m = 0.5f;
6 if (d.X != 0 && d.Y != 0)
7 {
8 a = Math.Atan(d.Y / d.X);
9 }
10 else if (d.X == 0 && d.Y > 0)
11 {
12 m = 0.0f;
13 }
14 if (d.X < 0)
15 {
16 a -= (Math.PI * m);
17 }
18 else
19 {
20 a += (Math.PI * m);
21 }
22 if (a < 0)
23 {
24 a += (Math.PI * 2);
25 }
26 m_enddirection = (float)a;
27 }
这个方法的主要的问题是你只可以找到直角三角形的角度,也就是说你不能使用这个计算并且马上找到这个角。首先是它不能计算这个角度,如果这个方向是直接UP的,所以它只是计算不是在这种情况下。否则一个旋转的1/4的modifier既不增加也不减去从产生的这个角度,除非角度是直角。因为X被用来作为三角法的邻近部分计算它是同样对modifier作出决定,如果这个X部分的不同是负的,modifier被减否则它被加。这是因为如果你划分X和Y部分,并且他们中的一个是负的,那么另外的不是负的。你会产生一个负的角度,一个负的角度将使角度接近0的位置,一个负的角度将会使它接近Pi(旋转的一半),所以通过添加一半的Pi(旋转的四分之一)你可以计算角度在正X和通过减去你可以计算角度在一个负X。2 {
3 Vector2 d = m_endposistion - m_position;
4 double a = 0;
5 float m = 0.5f;
6 if (d.X != 0 && d.Y != 0)
7 {
8 a = Math.Atan(d.Y / d.X);
9 }
10 else if (d.X == 0 && d.Y > 0)
11 {
12 m = 0.0f;
13 }
14 if (d.X < 0)
15 {
16 a -= (Math.PI * m);
17 }
18 else
19 {
20 a += (Math.PI * m);
21 }
22 if (a < 0)
23 {
24 a += (Math.PI * 2);
25 }
26 m_enddirection = (float)a;
27 }
最后你检查这个角度是负的,如果它不是,你增加1去保证它是并且设置新的结束方向到这个值中。
在这个例子里提供了,当你点击鼠标右键,它给这个unit一个新的目标去移动。
源代码:http://www.ziggyware.com/readarticle.php?article_id=153
(完)