汉诺塔自动解题动画中的iOS开发技巧
引
前段時間做了一道題,要求實現漢諾塔游戲的自動解題動畫:
漢諾塔游戲應該都了解規則:
1、將盤子全部移動到塔C
2、每次只能移動一個圓盤;
3、大盤不能疊在小盤上面。
要求由用戶輸入盤子的數量,繪制盤子和塔,點擊開始后自動解題,并以動畫移動盤子的形式演示。
覺得還挺有意思的,而且在做的過程中也踩了一些坑,用了一些技巧和優化,因此記錄下來。
效果:
漢諾塔解法
這道題中漢諾塔的解法本身并不是難點。
1、如果只有一個盤子,那就直接從A移動到C;
2、如果有兩個盤子,那就要先把小盤子移動到B,然后大盤子移動到C,再把小盤子移動到C;
3、如果有三個盤子,那就要先把上面兩個盤子移動到B(借助C的輔助),然后把底下的大盤子移動到C,然后把B上的兩個盤子借助A移動到C;
……
4、如果有n個盤子,那就要先把上面n-1個盤子移動到B(借助C的輔助),然后把底下的大盤子移動到C,然后把B上的n-1盤子借助A移動到C。
綜上所述,除了一個盤子的情況直接移動,其余都需要借助其他盤子的幫助,復雜情況雖然不一樣,但是過程是遞歸不斷重復的。
遞歸代碼如下:
// 確定提交 - (void)submit {if ([self.numberField.text isEqualToString:@""]) {NSLog(@"未輸入內容");UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:@"您還未輸入層數!" preferredStyle:UIAlertControllerStyleAlert];UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {}];[alertController addAction:okAction];[self presentViewController:alertController animated:YES completion:nil];} else {self.diskNumber = [self.numberField.text integerValue];self.moveCount = 0;[self hanoiWithDisk:self.diskNumber towers:@"A" :@"B" :@"C"];NSLog(@">>移動了%ld次", self.moveCount);} }// 移動算法 - (void)hanoiWithDisk:(NSInteger)diskNumber towers:(NSString *)towerA :(NSString *)towerB :(NSString *)towerC {if (diskNumber == 1) {// 只有一個盤子則直接從A塔移動到C塔[self move:1 from:towerA to:towerC];} else {[self hanoiWithDisk:diskNumber-1 towers:towerA :towerC :towerB];// 遞歸把A塔上編號1~diskNumber-1的盤子移動到B塔,C塔輔助[self move:diskNumber from:towerA to:towerC];// 把A塔上編號為diskNumber的盤子移動到C塔[self hanoiWithDisk:diskNumber-1 towers:towerB :towerA :towerC];// 遞歸把B塔上編號1~diskNumber-1的盤子移動到C塔,A塔輔助} }// 移動過程 - (void)move:(NSInteger)diskIndex from:(NSString *)fromTower to:(NSString *)toTower {NSLog(@"第%ld次移動:把%ld號盤從%@移動到%@", ++self.moveCount, diskIndex, fromTower, toTower); }三層盤子時:
四層盤子時:
可見算法是正確的,接下來就是實現繪制和動畫的問題。
繪制塔和盤子
解決了算法的問題,下一步我們要繪制圖形了。
這里為了方便我決定全部用UIView來做,比如塔就是一橫一豎兩個UIView,每個盤子都是一個UIView。
為了方便給盤子編號,創建一個繼承自UIView的盤子類,加上編號屬性:
#pragma mark - Disk Model // 自定義的盤子模型,在UIView基礎上加上編號屬性 @interface OXDiskModel : UIView @property NSInteger index; @end@implementation OXDiskModel@end因為這個代碼很短,沒必要新開一個文件,直接在繪制圖形的ViewController.m文件中加上這個代碼就可以實現了。
對于塔,一開始我直接在界面上繪制三個塔的6條線,很簡單,但是在涉及到動畫的時候,需要頻繁用到每個塔的位置以及塔上已有的盤子數量才能確定盤子移動到的位置,這就很麻煩,而且不穩定,代碼很復雜。
后來我改成把塔也抽象出來成一個塔類,在其類中繪制兩條線,并且加上塔名稱以及塔上盤子數量的屬性,這樣就可以直接調用了,在遞歸算法中,我們可以直接傳遞三個塔對象,可以很方便地計算,減少了大量的代碼,代碼結構也更加清晰。
塔的繪制代碼和屬性就不寫出來了,有單獨的類文件,可以直接在工程中看,這里直說思想,對于一些適合抽離出來的對象,我們應該盡可能抽象成對應的類,將它的操作、行為、屬性等放在類中寫,可以極大地簡化代碼、使代碼結構更清晰。
這樣,我們就可以根據屏幕大小算出每個塔合適的大小,然后去創建三個塔對象,添加到界面上就好了。
// 三座塔 - (void)initThreeTower {// 添加三座塔NSInteger height = (SCREENHEIGHT - 150)/3 - 30;for (int i = 0; i < 3; i++) {OXTowerView *tower = [[OXTowerView alloc] initWithFrame:CGRectMake((SCREENWIDTH-250)/2, 130 + (height+30)*i, 250, height+5)];tower.diskNumber = 0;[self.view addSubview:tower];[self.towerArray addObject:tower];// 塔號UILabel *towerLabel = [[UILabel alloc] initWithFrame:CGRectMake(12, tower.frame.origin.y + height + 5, SCREENWIDTH-24, 15)];switch (i) {case 0:towerLabel.text = @"A";tower.towerId = @"A";tower.diskNumber = self.diskNumber;// 一開始盤子都在塔A上break;case 1:towerLabel.text = @"B";tower.towerId = @"B";break;case 2:towerLabel.text = @"C";tower.towerId = @"C";break;default:break;}towerLabel.textColor = [UIColor darkGrayColor];towerLabel.textAlignment = NSTextAlignmentCenter;towerLabel.font = [UIFont systemFontOfSize:14];[self.view addSubview:towerLabel];} }然后根據輸入的盤子層數,動態算出每個盤子合適的高度以及每個盤子的寬度(從大到小),放在第一個塔上:
// 初始放置盤子 - (void)initWithDiskPut {NSInteger towerHeight = (SCREENHEIGHT - 150)/3 - 40;NSInteger diskHeight = towerHeight / self.diskNumber;// 盤子高度// 依次放置盤子for (int i = 0; i < self.diskNumber; i++) {NSInteger diskWeight = 230 - 30*i;// 盤子寬度// 自定義的盤子模型類OXDiskModel *disk = [[OXDiskModel alloc] initWithFrame:CGRectMake((SCREENWIDTH-diskWeight)/2, 140 + diskHeight*(self.diskNumber-i-1), diskWeight, diskHeight)];disk.backgroundColor = [UIColor yellowColor];disk.layer.borderColor = [[UIColor darkGrayColor] CGColor];disk.layer.borderWidth = 1;disk.index = self.diskNumber - i;[self.view addSubview:disk];[self.diskArray addObject:disk];} }動畫解題
在繪制過程中我們充分利用了面向對象編程的思想。現在來到最后一個問題,把算法和動畫結合起來。
算法還是那個算法,在之前的算法中,我們傳遞的參數只是簡單的字符串來代替三個塔,盤子也只是用盤子編號來代替,這里我們就要用我們的塔對象和盤子對象來作為真正的參數傳遞了。
對于塔,我們直接傳遞塔對象;對于盤子,我們傳參還是傳盤子編號,但是我們用一個數組記錄所有盤子,然后循環找到當前要移動的對應編號的盤子。
盤子的移動動畫我們使用簡單的UIView動畫就可以實現了,關于UIView基礎動畫可以看這篇文章:傳送門:iOS基礎動畫教程。
在動畫block中,我們去改變盤子的center,也就是中心點的Y坐標,來達到移動的目的,如何計算出要移動到哪呢?從參數中我們可以知道要移動到哪個塔,根據塔的屬性可以知道塔上現在有多少個盤子,那么就可以根據塔的坐標、塔上盤子的數量、每個盤子的高度來計算出這個盤子要移動到哪個坐標了。
UIView動畫有一個completion block,用來在動畫完成后執行一些操作,上面我們要用到塔上的盤子數量,那在移動完后我們一定也要更新每座塔的數量,移走的塔數量減一,移到的塔數量加一。
這里就可以體現把塔作為對象的好處了, 試想一下不這么做,我們如果要知道每座塔的坐標以及每座塔上的盤子數量,一定要用數組去記錄,而且傳參時我們只能像最開始一樣傳遞塔名字符串,那還得根據這個字符串來判斷改變數組中的第幾個元素的塔數量,獲取哪個塔坐標,這都增加了很多代碼量。但是有了塔對象,我們可以直接作為參數傳遞,也可以直接獲取盤子數量去修改,太方便了。
// 開始 - (void)start {self.moveCount = 0;[self hanoiWithDisk:self.diskNumber towers:@"A" :@"B" :@"C"];NSLog(@">>移動了%ld次", self.moveCount); }// 移動算法 - (void)hanoiWithDisk:(NSInteger)diskNumber towers:(OXTowerView *)towerA :(OXTowerView *)towerB :(OXTowerView *)towerC {if (diskNumber == 1) {// 只有一個盤子則直接從A塔移動到C塔[self move:1 from:towerA to:towerC];} else {[self hanoiWithDisk:diskNumber-1 towers:towerA :towerC :towerB];// 遞歸把A塔上編號1~diskNumber-1的盤子移動到B塔,C塔輔助[self move:diskNumber from:towerA to:towerC];// 把A塔上編號為diskNumber的盤子移動到C塔[self hanoiWithDisk:diskNumber-1 towers:towerB :towerA :towerC];// 遞歸把B塔上編號1~diskNumber-1的盤子移動到C塔,A塔輔助} }// 移動過程 - (void)move:(NSInteger)diskIndex from:(OXTowerView *)fromTower to:(OXTowerView *)toTower {NSLog(@"第%ld次移動:把%ld號盤從%@移動到%@", ++self.moveCount, diskIndex, fromTower, toTower);for (OXDiskModel *disk in self.diskArray) {if (disk.index == diskIndex) {[UIView animateWithDuration:1.0 animations:^{// 計算改變盤子位置} completion:^(BOOL finished) {if (finished) {// 動畫完成// 更新塔上的盤子數量fromTower.diskNumber--;toTower.diskNumber++;}}];}} }這里有一個有意思的點可以看一下移動算法中后面三個塔參數前面是沒有文字的,只有一個冒號,OC支持定義方法時參數前不需要一定要有文字,只不過為了方便理解都會加一個參數說明。
到此,是不是問題都解決了?不是的,如果你直接這么寫,運行后會發現所有動畫都一起移動到塔C,根本沒有過程!這是為什么?
因為算法運行得很快,而動畫需要時間,這就導致還沒開始動畫,所有的算法都計算完了,最后只會把所有盤子一起移動到塔C,因為那就是算法最后算出來的目標位置。
這時我想到的第一個方法是用dispatch_semaphore_t來做為信號量,控制算法等待動畫完畢后再進行,用法說明可以看這篇文章:傳送門:iOS之利用GCD信號量控制并發網絡請求,比如像下面這樣:
// 移動過程 - (void)move:(NSInteger)diskIndex from:(OXTowerView *)fromTower to:(OXTowerView *)toTower {dispatch_semaphore_t sema = dispatch_semaphore_create(0);// 初始化信號量為0NSLog(@"第%ld次移動:把%ld號盤從%@移動到%@", ++self.moveCount, diskIndex, fromTower, toTower);for (OXDiskModel *disk in self.diskArray) {if (disk.index == diskIndex) {[UIView animateWithDuration:1.0 animations:^{// 計算改變盤子位置} completion:^(BOOL finished) {if (finished) {// 動畫完成// 更新塔上的盤子數量fromTower.diskNumber--;toTower.diskNumber++;dispatch_semaphore_signal(sema);// 增加信號量,結束等待}}];break;}}dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);// 信號量若沒增加,則一直等待,直到動畫完成 }運行后會發現動畫干脆都不動了,為什么?因為動畫在主線程,信號量等待也在主線程,那就造成了“信號量等待信號才能繼續往下進行<–>動畫在主線程中被信號量卡主等待,無法進行,但是進行完了才能給出信號量”的循環等待。
這怎么解決?其實看上面的解釋就能夠想到辦法了,把算法放到分線程去跑,動畫放在主線程!這樣信號量等待是讓分線程等待,不會影響主線程,這樣就不會阻塞,同時可以實現算法等待動畫完畢后再進行的效果,完美:
// 開始移動 - (void)beginMove {self.moveCount = 0;WeakSelfdispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{// 到分線程去處理算法StrongSelfif (strongSelf) {[strongSelf hanoiWithDisk:strongSelf.diskNumber towers:[strongSelf.towerArray objectAtIndex:0] :[strongSelf.towerArray objectAtIndex:1] :[strongSelf.towerArray objectAtIndex:2]];}});// NSLog(@">>移動了%ld次", self.moveCount);}// 移動算法 - (void)hanoiWithDisk:(NSInteger)diskNumber towers:(OXTowerView *)towerA :(OXTowerView *)towerB :(OXTowerView *)towerC {if (diskNumber == 1) {// 只有一個盤子則直接從A塔移動到C塔[self move:1 from:towerA to:towerC];} else {[self hanoiWithDisk:diskNumber-1 towers:towerA :towerC :towerB];// 遞歸把A塔上編號1~diskNumber-1的盤子移動到B塔,C塔輔助[self move:diskNumber from:towerA to:towerC];// 把A塔上編號為diskNumber的盤子移動到C塔[self hanoiWithDisk:diskNumber-1 towers:towerB :towerA :towerC];// 遞歸把B塔上編號1~diskNumber-1的盤子移動到C塔,A塔輔助} }// 移動過程 - (void)move:(NSInteger)diskIndex from:(OXTowerView *)fromTower to:(OXTowerView *)toTower {dispatch_semaphore_t sema = dispatch_semaphore_create(0);// 初始化信號量為0NSLog(@"第%ld次移動:把%ld號盤從塔%@移動到塔%@", ++self.moveCount, diskIndex, fromTower.towerId, toTower.towerId);for (OXDiskModel *disk in self.diskArray) {if (disk.index == diskIndex) {WeakSelfdispatch_async(dispatch_get_main_queue(), ^{// 切回主線程進行移動動畫[UIView animateWithDuration:1.0 animations:^{StrongSelfif (strongSelf) {// 改變盤子的位置CGPoint diskCenter = disk.center;NSInteger towerY = 10 + toTower.frame.origin.y;NSInteger towerHeight = toTower.frame.size.height-15;NSInteger diskHeight = towerHeight / strongSelf.diskNumber;// 每個盤子高度NSInteger hasDiskHieght = diskHeight * toTower.diskNumber;// 已放置了的盤子高度diskCenter.y = towerY + (towerHeight - hasDiskHieght) - diskHeight/2;disk.center = diskCenter;}} completion:^(BOOL finished) {if (finished) {// 動畫完成StrongSelfif (strongSelf) {// 改變fromTower的盤子數量fromTower.diskNumber--;// 改變toTower的盤子數量toTower.diskNumber++;dispatch_semaphore_signal(sema);// 增加信號量,結束等待}}}];});break;}}dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);// 信號量若沒增加,則一直等待,直到動畫完成 }這時候再運行就可以完美實現效果了:
結
為了解決阻塞的問題,還嘗試過延遲執行、動畫隊列等方法,但都不如這個方法簡單有效。
在做這個的過程中,用到了很多小技巧,也多次優化了代碼,對于我自己來說代碼越來越賞心悅目,實在是一次很好的學習訓練的經驗。
而且看著自己做的漢諾塔游戲自動動畫解題很有意思不是嘛!
示例工程:https://github.com/Cloudox/OXHanoiDemo
版權所有:http://blog.csdn.net/cloudox_
總結
以上是生活随笔為你收集整理的汉诺塔自动解题动画中的iOS开发技巧的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2020.3.31Java学习笔记
- 下一篇: android车载无线充apple,绿联