13.6.9 两个转折点的连接
两个转折点的连接是最复杂的一种连接情况,因为两个转折点又可分为如下几种情况。
p1、p2位于同一行,但不能直接相连,就必须有两个转折点,分向上与向下两种连接情况。
p1、p2位于同一列,但不能直接相连,也必须有两个转折点,分向左与向右两种连接情况。
p2在p1的右下角,有6种转折情况。
p2在p1的右上角,同样有6种转折情况。
提示:
对于p2位于p1的左上角、左下角的情况,同样只要把p1、p2的位置互换即可。
对于上面4种情况,同样需要分别进行处理。
p1、p2位于同一行,但它们不能直接相连,因此必须有两个转折点,图13.13显示了这种相连的示意图。
从图13.13可以看到,当p1与p2位于同一行但不能直接相连时,这两个点既可在上面相连,也可在下面相连,这两种情况都代表它们可以相连。我们先把这两种情况都加入结果中,最后计算最近的距离。
实现时可以先构建一个NSDictionary,NSDictionary的key为第一个转折点,NSDictionary的value为第二个转折点(每种连接情况最多只有两个连接点),如NSDictionary的count大于1,说明这两个FKPoint有多种连接途径,那么程序还需要计算路径最小的连接方式。
p1、p2位于同一列,但它们不能直接相连,因此必须有两个转折点,图13.14显示了这种相连的示意。

图13.13同一行不能直接相连
图13.14同一列不能直接相连
从图13.14可以看到,当p1与p2位于同一列但不能直接相连时,这两个点既可在左边相连,也可在右边相连,这两种情况都代表它们可以相连。我们先把这两种情况都加入结果中,最后计算最近的距离。
实现的方法与同一行不能直接相连的情况相同。
p2位于p1右下角时,一共可能出现6种连接情况,图13.15~图13.20分别绘制了这6种连接情况。
图13.15p2位于p1右下角有两个转折点的情况1
图13.16p2位于p1右下角有两个转折点的情况2
图13.17p2位于p1右下角有两个转折点的情况3
图13.18p2位于p1右下角有两个转折点的情况4
图13.19p2位于p1右下角有两个转折点的情况5
图13.20p2位于p1右下角有两个转折点的情况6
实际上,p2还可能位于p1的右上角,出现的6种连接情形与此相似,此处不再详述。
接下来定义一个getLinkPoints方法对具有两个连接点的情况进行处理。
程序清单:codes/13/Link/Link/sources/board/FKGameService.m
<a href="http://s3.51cto.com/wyfs02/M02/12/6B/wKioL1MGsDzRrbXnAAXBvNKuI-k915.jpg" target="_blank"></a>
<a href="http://s3.51cto.com/wyfs02/M01/12/6A/wKiom1MGsG6z2V24AASJRANw8ik750.jpg" target="_blank"></a>
程序中的粗体字代码分别调用getYLinkPoints: p2Chanel: pieceHeight:、getXLinkPoints: p2Chanel: pieceWidth:方法来收集各种可能出现的连接路径,两个方法的代码如下。
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<code>/**</code>
<code> </code><code>* 遍历两个集合,先判断第一个集合中元素的x坐标与另一个集合中元素的x坐标是否相同(纵向),</code>
<code> </code><code>* 如果相同,即在同一列,再判断是否有障碍,没有则加到NSMutableDictionary中</code>
<code> </code><code>* @return 存放可以纵向直线连接的连接点的键值对</code>
<code> </code><code>*/</code>
<code>- (NSDictionary*) getYLinkPoints:(NSArray*) p1Chanel</code>
<code> </code><code>p2Chanel:(NSArray*) p2Chanel pieceHeight:(NSInteger) pieceHeight</code>
<code>{</code>
<code> </code><code>NSMutableDictionary* result = [[NSMutableDictionary alloc]init];</code>
<code> </code><code>for</code> <code>(</code><code>int</code> <code>i = 0; i < p1Chanel.count; i++)</code>
<code> </code><code>{</code>
<code> </code><code>FKPoint* temp1 = [p1Chanel objectAtIndex:i];</code>
<code> </code><code>for</code> <code>(</code><code>int</code> <code>j = 0; j < p2Chanel.count; j++)</code>
<code> </code><code>{</code>
<code> </code><code>FKPoint* temp2 = [p2Chanel objectAtIndex:j];</code>
<code> </code><code>// 如果x坐标相同(在同一列)</code>
<code> </code><code>if</code> <code>(temp1.x == temp2.x)</code>
<code> </code><code>{</code>
<code> </code><code>// 没有障碍则加到结果的NSMutableDictionary中</code>
<code> </code><code>if</code> <code>(![self isYBlockFromP1:temp1 toP2:temp2 pieceHeight:pieceHeight])</code>
<code> </code><code>{</code>
<code> </code><code>[result setObject:temp2 forKey:temp1];</code>
<code> </code><code>}</code>
<code> </code><code>}</code>
<code> </code><code>}</code>
<code> </code><code>}</code>
<code> </code><code>return</code> <code>[result copy];</code>
<code>}</code>
<code> </code><code>* 遍历两个集合,先判断第一个集合中元素的y坐标与另一个集合中元素的y坐标是否相同(横向),</code>
<code> </code><code>* 如果相同,即在同一行,再判断是否有障碍,没有则加到NSMutableDictionary中</code>
<code> </code><code>* @return 存放可以横向直线连接的连接点的键值对</code>
<code>- (NSDictionary*) getXLinkPoints:(NSArray*) p1Chanel</code>
<code> </code><code>p2Chanel:(NSArray*) p2Chanel pieceWidth:(NSInteger) pieceWidth</code>
<code> </code><code>// 从第一通道中取一个点</code>
<code> </code><code>// 再遍历第二个通道,看第二通道中是否有点可以与temp1横向相连</code>
<code> </code><code>// 如果y坐标相同(在同一行),再判断它们之间是否有直接障碍</code>
<code> </code><code>if</code> <code>(temp1.y == temp2.y)</code>
<code> </code><code>if</code> <code>(![self isXBlockFromP1:temp1 toP2:temp2 pieceWidth:pieceWidth])</code>
<code> </code><code>// 没有障碍则加到结果的NSMutableDictionary中</code>
经过上面的实现之后,getLinkPointsFromPoint: toPoint: width: height:方法可以找出point1、point2两个点之间所有可能的连接情况,该方法返回一个NSDictionary对象,NSDictionary中每个key-value对代表一种连接情况,其中key代表第一个连接点,value代表第二个连接点。
当point1、point2之间有多种连接情况时,程序还需要找出所有连接情况中的最短路径,link(Piece p1, Piece p2)方法中的④号粗体字代码调用了getShortcutFromPoint: toPoint: turns: distance:方法进行处理,下面进行详细分析。
13.6.10 找出最短距离
为了找出所有连接情况中的最短路径,程序实现可分为两步。
①遍历转折点NSDictionary中的所有key-value对,与原来选择的两个点构成一个FKLinkInfo。每个FKLinkInfo代表一条完整的连接路径,并将这些FKLinkInfo收集成一个NSArray集合。
②遍历第1步得到的NSArray集合,计算每个FKLinkInfo中所有连接点的总距离,选取与最短距离相差最小的FKLinkInfo返回即可。
下面的方法实现了上面的思路。
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<code> </code><code>* 获取p1和p2之间最短的连接信息</code>
<code> </code><code>* @param p1 第一个点</code>
<code> </code><code>* @param p2 第二个点</code>
<code> </code><code>* @param turns 放转折点的NSDictionary</code>
<code> </code><code>* @param shortDistance 两点之间的最短距离</code>
<code> </code><code>* @return p1和p2之间最短的连接信息</code>
<code>- (FKLinkInfo*) getShortcutFromPoint:(FKPoint*) p1 toPoint:(FKPoint*) p2</code>
<code> </code><code>turns:(NSDictionary*) turns distance:(NSInteger)shortDistance</code>
<code> </code><code>NSMutableArray* infos = [[NSMutableArray alloc] init];</code>
<code> </code><code>// 遍历结果NSDictionary</code>
<code> </code><code>for</code> <code>(FKPoint* point1 in turns)</code>
<code> </code><code>FKPoint* point2 = turns[point1];</code>
<code> </code><code>// 将转折点与选择点封装成FKLinkInfo对象,放到NSArray集合中</code>
<code> </code><code>[infos addObject:[[FKLinkInfo alloc]</code>
<code> </code><code>initWithP1:p1 p2:point1 p3:point2 p4:p2]];</code>
<code> </code><code>return</code> <code>[self getShortcut:infos shortDistance:shortDistance];</code>
<code> </code><code>* 从infos中获取连接线最短的那个FKLinkInfo对象</code>
<code> </code><code>* @param infos</code>
<code> </code><code>* @return 连接线最短的那个FKLinkInfo对象</code>
<code>- (FKLinkInfo*) getShortcut:(NSArray*) infos shortDistance:(</code><code>int</code><code>) shortDistance</code>
<code> </code><code>int</code> <code>temp1 = 0;</code>
<code> </code><code>FKLinkInfo* result = nil;</code>
<code> </code><code>for</code> <code>(</code><code>int</code> <code>i = 0; i < infos.count; i++)</code>
<code> </code><code>FKLinkInfo* info = [infos objectAtIndex:i];</code>
<code> </code><code>// 计算出几个点的总距离</code>
<code> </code><code>NSInteger distance = [self countAll:info.points];</code>
<code> </code><code>// 将循环第一个的差距用temp1保存</code>
<code> </code><code>if</code> <code>(i == 0)</code>
<code> </code><code>temp1 = distance - shortDistance;</code>
<code> </code><code>result = info;</code>
<code> </code><code>// 如果下一次循环的值比temp1还小, 则用当前的值作为temp1</code>
<code> </code><code>if</code> <code>(distance - shortDistance < temp1)</code>
<code> </code><code>return</code> <code>result;</code>
<code> </code><code>* 计算NSArray中所有点的距离总和</code>
<code> </code><code>* @param points 需要计算的连接点</code>
<code> </code><code>* @return 所有点的距离总和</code>
<code>- (NSInteger) countAll:(NSArray*) points</code>
<code> </code><code>NSInteger result = 0;</code>
<code> </code><code>for</code> <code>(</code><code>int</code> <code>i = 0; i < points.count - 1; i++)</code>
<code> </code><code>// 获取第i个点</code>
<code> </code><code>FKPoint* point1 = [points objectAtIndex:i];</code>
<code> </code><code>// 获取第i + 1个点</code>
<code> </code><code>FKPoint* point2 = [points objectAtIndex:i + 1];</code>
<code> </code><code>// 计算第i个点与第i + 1个点的距离,并添加到总距离中</code>
<code> </code><code>result += [self getDistanceFromPoint:point1 toPoint:point2];</code>
<code> </code><code>* 获取两个点之间的最短距离</code>
<code> </code><code>* @return 两个点的距离距离总和</code>
<code>- (CGFloat) getDistanceFromPoint:(FKPoint*) p1 toPoint:(FKPoint*) p2</code>
<code> </code><code>int</code> <code>xDistance = </code><code>abs</code><code>(p1.x - p2.x);</code>
<code> </code><code>int</code> <code>yDistance = </code><code>abs</code><code>(p1.y - p2.y);</code>
<code> </code><code>return</code> <code>xDistance + yDistance;</code>
至此,《疯狂连连看》游戏中两个方块可能相连的所有情况都处理完成了,应用程序即可调用FKGameService所提供的(FKLinkInfo*) linkWithBeginPiece:(FKPiece*)p1 endPiece: (FKPiece*) p2方法来判断两个方块是否可以相连,这个过程也是编写该游戏最烦琐的地方。
通过对《疯狂连连看》游戏的分析与开发,读者应该发现编写一个游戏并没有想象的那么难,开发者需要冷静、条理化的思维,先分析游戏中所有可能出现的情况,然后在程序中对所有的情况进行判断,并进行相应的处理。
本程序中FKGameService组件的实现思路与《疯狂Android讲义》中Android版《疯狂连连看》游戏的实现思路基本相同,笔者无法保证这种实现方式为最优算法。这种算法实现起来有些烦琐,但它的条理十分清晰,非常适合初、中级程序员学习。
13.7小结
本章介绍了一款常见的单机休闲类游戏——iOS版的《疯狂连连看》,这款流行的小游戏的开发难度适中,而且能充分激发学习热情,对iOS学习者来说是一个不错的选择。学习本章需要重点掌握单机游戏的界面分析与数据建模的能力:游戏玩家眼中看到的是游戏界面,开发者眼中看到的应该是数据模型。除此之外,单机游戏通常总会有一个比较美观的界面,因此,通常都需要通过自定义UIView来实现游戏主界面。《疯狂连连看》游戏中需要判断两个方块(图片)是否可以相连,这需要开发者对两个方块的位置分别进行处理,并针对不同的情况提供相应的实现,这也是开发单机游戏需要重点掌握的能力。
——————本文节选自《疯狂ios讲义(上)》
本文转自
fkJava李刚 51CTO博客,原文链接:http://blog.51cto.com/javaligang/1361533 ,如需转载请自行联系原作者