[Silverlight动画]转向行为 - 对象回避
对象回避主题的完整意义是指,在机车行走的路线中存在一些障碍物,机车必须绕开、防止触碰到它们。听上去和碰撞检测有关,然而这仅仅是发生在预测阶段,也就是:“以我当前的速度行驶下去,可能就会撞到它了。”
既然碰撞是预测的,就得长点脑子确保碰撞不会发生。你可能正幼稚的想,那停下来或者调头不就行了嘛,你忘了有很多行为是在同时发生着的。如果要躲避的是一个食肉动物,光靠停下来或者躲在树后面显然是不明智的。凡有脑子的,此时会采取一切手段来躲避,而食肉动物也同样会绕开障碍物来追捕你。
另外,如果要避开一个非常接近的东西,就必须改变路线。可能在沙漠中,发现远处有座金字塔,稍作调整甚至不作调整的继续前进都不会碰到它。而如果金字塔就在你面前,为了不撞上去,起码要转差不多90度左右。
现在了解了该行为的复杂程度,以及为什么存在那么多不同的实现方式了吧。在大多数解决方案中,首先把障碍物看作是一个圆(3D中是球)。实际上障碍物可能并不是圆,但为了计算方便,还是把它们想象成一个有中心点和半径的对象。注意,通常情况下碰撞检测不需要严格到像素级别,只要大致的知道其大小和位置即可,然后设法绕开它。这里是用来描述障碍物的圆类:
public partial class Circle : UserControl { private double _radius; private Color _color; public double Radius { get { return _radius; } set { _radius = value; } } public Vector2D position { get { return new Vector2D(_compositeTransform.TranslateX, _compositeTransform.TranslateY); } } private CompositeTransform _compositeTransform; private TransformGroup _transformGroup; public Circle() { InitializeComponent(); Loaded += new RoutedEventHandler(Circle_Loaded); } public void init(double radius,Color color) { xCircle.Width = radius * 2; xCircle.Height = radius * 2; xCircle.Fill = new SolidColorBrush(color); xCircle.Margin = new Thickness(-radius, -radius, 0, 0); _radius = radius; _color = color; } void Circle_Loaded(object sender, RoutedEventArgs e) { var transformGroup = this.RenderTransform as TransformGroup; if (transformGroup == null) { _transformGroup = new TransformGroup(); this.RenderTransform = _transformGroup; _compositeTransform = new CompositeTransform(); _transformGroup.Children.Add(_compositeTransform); } } public double x { get { return _compositeTransform.TranslateX; } set { _compositeTransform.TranslateX = value; } } public double y { get { return _compositeTransform.TranslateY; } set { _compositeTransform.TranslateY = value; } } }
这个类很简单,通过半径和颜色画出一个圆,并且有两个只读属性,半径和位置。由于是在向量环境中计算,所以位置返回一个2D向量。现在开始讲述回避行为的实现。
由于要回避的对象通常不止一个,所以回避函数通过对一个数组的遍历来确认哪些需要被避开。为此,会计算出一个转向力。
private double _avoidDistance = 300; private double _avoidBuffer = 20; public void avoid(List<Circle> circles) { foreach (var circle in circles) { Vector2D heading = _velocity.clone().normalize(); //障碍物和机车的位移向量 Vector2D difference = circle.position.subtract(_postion); double dorProd = difference.dotProd(heading); //如果障碍物在机车前方 if (dorProd>0) { //机车的触角 Vector2D feeler = heading.multiply(_avoidDistance); //位移在触角上的映射 Vector2D projection = heading.multiply(dorProd); //障碍物离触角的距离 double dist = projection.subtract(difference).length; //如果触角在计算上缓冲后和障碍物相交并且位移的映射的长度小于触角的长度,我们就说是将要发生碰撞,需要改变运行方向 if (dist<circle.Radius+_avoidBuffer&&projection.length<feeler.length) { //计算出一个90度的转向力 Vector2D force = heading.multiply(_maxSpeed); force.angle += difference.sign(_velocity) * Math.PI / 2; //通过距离障碍物的距离,调整力度大小,使之足够小但又能避开 force = force.multiply(1.0 - projection.length / feeler.length); // 叠加与转向力上 _steeringForce = _steeringForce.add(force); //刹车,转弯时要放慢机车的速度,里障碍物越近刹车力越大 _velocity = _velocity.multiply(projection.length / feeler.length); } } } }
测试:
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Steer" xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing" mc:Ignorable="d" x:Class="Steer.AvoidTest" d:DesignWidth="640" d:DesignHeight="480"> <Grid x:Name="LayoutRoot"> <local:SteeredVehicle x:Name="myWander" HorizontalAlignment="Left" Height="40" VerticalAlignment="Top" Width="40" RenderTransformOrigin="0.5,0.5"> <ed:RegularPolygon Fill="Blue" Height="40" InnerRadius="1" PointCount="3" Stretch="Fill" Stroke="Black" UseLayoutRounding="False" Width="40" RenderTransformOrigin="0.5,0.5" StrokeThickness="0"> <ed:RegularPolygon.RenderTransform> <CompositeTransform Rotation="90"/> </ed:RegularPolygon.RenderTransform> </ed:RegularPolygon> </local:SteeredVehicle> <local:Circle x:Name="Circle1" HorizontalAlignment="Left" VerticalAlignment="Top"/> <local:Circle x:Name="Circle2" HorizontalAlignment="Left" VerticalAlignment="Top"/> </Grid> </UserControl>
public partial class AvoidTest : UserControl { List<Circle> circles; public AvoidTest() { // Required to initialize variables InitializeComponent(); Loaded += new RoutedEventHandler(AvoidTest_Loaded); } void AvoidTest_Loaded(object sender, RoutedEventArgs e) { myWander.position = new Vector2D(200, 200); Circle1.init(100, Colors.Orange); Circle1.x = 600; Circle1.y = 200; Circle2.init(70, Colors.Red); Circle2.x = 100; Circle2.y = 300; circles = new List<Circle>(); circles.Add(Circle1); circles.Add(Circle2); CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering); } void CompositionTarget_Rendering(object sender, EventArgs e) { myWander.wander(); myWander.avoid(circles); myWander.update(); } }