lead
I did a problem some time ago and asked to realize the automatic problem-solving animation of Hanoi tower game:

image.png
Hanoi tower game should know the rules:
1. Move all plates to tower C 2. Only one disc can be moved at a time; 3. The large plate cannot be stacked on the small plate.
The user is required to input the number of plates, draw plates and towers, click start to solve the problem automatically, and demonstrate it in the form of animated moving plates.
I think it's quite interesting, and I stepped on some pits and used some skills and Optimization in the process of doing it, so I recorded it.
effect:

Hanoi Tower solution
In this problem, the solution of Hanoi Tower itself is not a difficulty.
1. If there is only one plate, move directly from A to C; 2. If there are two plates, first move the small plate to B, then move the large plate to C, and then move the small plate to C; 3. If there are three plates, first move the upper two plates to B (with the help of C), then move the lower large plate to C, and then move the two plates on B to C with the help of A; ...... 4. If there are n plates, first move the upper n-1 plate to B (with the help of C), then move the lower large plate to C, and then move the N-1 plate on B to C with the help of A.
To sum up, except that one plate moves directly, others need the help of other plates. Although the complex situation is different, the process is recursive and repeated.
The recursive code is as follows:
// Confirm submission - (void)submit { if ([self.numberField.text isEqualToString:@""]) { NSLog(@"No content entered"); UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Tips" message:@"You have not entered the number of layers!" preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"determine" 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(@">>Moved%ld second", self.moveCount); } } // Mobile algorithm - (void)hanoiWithDisk:(NSInteger)diskNumber towers:(NSString *)towerA :(NSString *)towerB :(NSString *)towerC { if (diskNumber == 1) {// Only one plate moves directly from tower A to tower C [self move:1 from:towerA to:towerC]; } else { [self hanoiWithDisk:diskNumber-1 towers:towerA :towerC :towerB];// Recursively move the plates numbered 1~diskNumber-1 on tower A to Tower B and tower C for assistance [self move:diskNumber from:towerA to:towerC];// Move the plate numbered diskNumber on tower A to tower C [self hanoiWithDisk:diskNumber-1 towers:towerB :towerA :towerC];// Recursively move the plates numbered 1~diskNumber-1 on Tower B to tower C, assisted by tower A } } // Moving process - (void)move:(NSInteger)diskIndex from:(NSString *)fromTower to:(NSString *)toTower { NSLog(@"The first%ld Next move: move%ld No. plate from%@Move to%@", ++self.moveCount, diskIndex, fromTower, toTower); }
When three layers of plates:

image.png
On the fourth floor:

It can be seen that the algorithm is correct. The next step is to realize rendering and animation.
Draw towers and plates
After solving the problem of algorithm, we will draw graphics in the next step.
Here, for convenience, I decided to use UIView to do it all. For example, the tower has two uiviews, one horizontal and one vertical. Each plate is a UIView.
To facilitate numbering plates, create a plate class inherited from UIView and add the number attribute:
#pragma mark - Disk Model // For the customized plate model, the number attribute is added on the basis of UIView @interface OXDiskModel : UIView @property NSInteger index; @end @implementation OXDiskModel @end
Because the code is very short, there is no need to open a new file directly in the viewcontroller for drawing graphics M file can be implemented by adding this code.
For the tower, at the beginning, I directly drew six lines of three towers on the interface, which is very simple, but when it comes to animation, I need to frequently use the position of each tower and the number of plates on the tower to determine the position of the plates. This is very troublesome, unstable and complex.
Later, I abstracted the tower into a tower class, drew two lines in its class, and added the attributes of the tower name and the number of plates on the tower, so that it can be called directly. In the recursive algorithm, we can directly pass three tower objects, which can be calculated easily, reduce a lot of code, and the code structure is clearer.
The drawing code and attributes of the tower are not written. There are separate class files that can be viewed directly in the project. Let's talk about the idea here. For some objects suitable for extraction, we should abstract them into corresponding classes as much as possible, and write their operations, behaviors and attributes in classes, which can greatly simplify the code and make the code structure clearer.
In this way, we can calculate the appropriate size of each tower according to the screen size, and then create three tower objects and add them to the interface.
// Three towers - (void)initThreeTower { // Add three towers 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]; // Tower number 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;// At first, the plates were on tower 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]; } }
Then, according to the input plate layers, dynamically calculate the appropriate height of each plate and the width of each plate (from large to small), and place it on the first tower:
// Initial placement plate - (void)initWithDiskPut { NSInteger towerHeight = (SCREENHEIGHT - 150)/3 - 40; NSInteger diskHeight = towerHeight / self.diskNumber;// Plate height // Place the plates in turn for (int i = 0; i < self.diskNumber; i++) { NSInteger diskWeight = 230 - 30*i;// Plate width // Custom plate model class 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]; } }
Animation problem solving
In the drawing process, we make full use of the idea of object-oriented programming. Now to the last question, combine the algorithm with animation.
The algorithm is still the same algorithm. In the previous algorithm, the parameters we passed are only simple strings to replace the three towers, and the plate is only replaced by the plate number. Here, we will pass our tower object and plate object as real parameters.
For towers, we pass the tower object directly; For plates, we pass parameters or plate numbers, but we use an array to record all plates, and then cycle to find the plate with the corresponding number to be moved.
We can use simple UIView animation to realize the moving animation of the plate. For the basic animation of UIView, see this article: Portal: iOS basic animation tutorial.
In the animation block, we change the center of the plate, that is, the Y coordinate of the center point, to achieve the purpose of moving. How to calculate where to move? From the parameters, we can know which tower to move to. According to the properties of the tower, we can know how many plates there are on the tower. Then we can calculate the coordinates of the plate to move according to the coordinates of the tower, the number of plates on the tower and the height of each plate.
UIView animation has a completion block, which is used to perform some operations after the animation is completed. Above, we need to use the number of plates on the tower. After moving, we must also update the number of each tower. The number of removed towers decreases by one and the number of moved towers increases by one.
Here we can reflect the benefits of taking the tower as an object. Imagine not doing so. If we want to know the coordinates of each tower and the number of plates on each tower, we must use the array to record. Moreover, when passing parameters, we can only pass the tower name string as at the beginning. Then we have to judge the number of towers that change the first few elements in the array according to the string, Obtaining which tower coordinates increases a lot of code. However, with the tower object, we can directly pass it as a parameter or directly obtain the number of plates to modify. It's too convenient.
// start - (void)start { self.moveCount = 0; [self hanoiWithDisk:self.diskNumber towers:@"A" :@"B" :@"C"]; NSLog(@">>Moved%ld second", self.moveCount); } // Mobile algorithm - (void)hanoiWithDisk:(NSInteger)diskNumber towers:(OXTowerView *)towerA :(OXTowerView *)towerB :(OXTowerView *)towerC { if (diskNumber == 1) {// Only one plate moves directly from tower A to tower C [self move:1 from:towerA to:towerC]; } else { [self hanoiWithDisk:diskNumber-1 towers:towerA :towerC :towerB];// Recursively move the plates numbered 1~diskNumber-1 on tower A to Tower B and tower C for assistance [self move:diskNumber from:towerA to:towerC];// Move the plate numbered diskNumber on tower A to tower C [self hanoiWithDisk:diskNumber-1 towers:towerB :towerA :towerC];// Recursively move the plates numbered 1~diskNumber-1 on Tower B to tower C, assisted by tower A } } // Moving process - (void)move:(NSInteger)diskIndex from:(OXTowerView *)fromTower to:(OXTowerView *)toTower { NSLog(@"The first%ld Next move: move%ld No. plate from%@Move to%@", ++self.moveCount, diskIndex, fromTower, toTower); for (OXDiskModel *disk in self.diskArray) { if (disk.index == diskIndex) { [UIView animateWithDuration:1.0 animations:^{ // Calculate and change the position of the plate } completion:^(BOOL finished) { if (finished) {// Animation complete // Update the number of plates on the tower fromTower.diskNumber--; toTower.diskNumber++; } }]; } } }
Here is an interesting point to see. In the mobile algorithm, there is no text in front of the last three tower parameters, only a colon. When OC supports the definition of methods, there is no text in front of the parameters, but a parameter description will be added for convenience of understanding.
So far, have all the problems been solved? No, if you write this directly, you will find that all animations move to tower C together after running. There is no process at all! Why?
Because the algorithm runs very fast and the animation takes time, the animation has not started yet. All the algorithms have been calculated. Finally, all the dishes will only be moved to tower C, because that is the target position finally calculated by the algorithm.
At this time, the first method I think of is to use dispatch_semaphore_t is used as the semaphore, and the control algorithm will wait until the animation is completed. See this article for instructions: Gateway: iOS uses GCD semaphores to control concurrent network requests For example:
// Moving process - (void)move:(NSInteger)diskIndex from:(OXTowerView *)fromTower to:(OXTowerView *)toTower { dispatch_semaphore_t sema = dispatch_semaphore_create(0);// The initialization semaphore is 0 NSLog(@"The first%ld Next move: move%ld No. plate from%@Move to%@", ++self.moveCount, diskIndex, fromTower, toTower); for (OXDiskModel *disk in self.diskArray) { if (disk.index == diskIndex) { [UIView animateWithDuration:1.0 animations:^{ // Calculate and change the position of the plate } completion:^(BOOL finished) { if (finished) {// Animation complete // Update the number of plates on the tower fromTower.diskNumber--; toTower.diskNumber++; dispatch_semaphore_signal(sema);// Increase semaphore and end waiting } }]; break; } } dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);// If the semaphore does not increase, wait until the animation is completed }
After running, you will find that the animation simply doesn't move. Why? Because the animation is in the main thread and the semaphore waiting is also in the main thread, there is a circular waiting that "the semaphore waits for the signal to continue < -- > the animation is stuck by the semaphore in the main thread and cannot be carried out, but the semaphore can be given only after it is completed".
How to solve this? In fact, you can think of a way by looking at the above explanation. Put the algorithm on the split line and put the animation on the main thread! In this way, semaphore waiting is to let the sub thread wait, which will not affect the main thread, so it will not be blocked. At the same time, it can realize the effect that the algorithm waits for the animation, which is perfect:
// Start moving - (void)beginMove { self.moveCount = 0; WeakSelf dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{// To branch process algorithm StrongSelf if (strongSelf) { [strongSelf hanoiWithDisk:strongSelf.diskNumber towers:[strongSelf.towerArray objectAtIndex:0] :[strongSelf.towerArray objectAtIndex:1] :[strongSelf.towerArray objectAtIndex:2]]; } }); // Nslog (@ "> > moved% ld times", self.moveCount); } // Mobile algorithm - (void)hanoiWithDisk:(NSInteger)diskNumber towers:(OXTowerView *)towerA :(OXTowerView *)towerB :(OXTowerView *)towerC { if (diskNumber == 1) {// Only one plate moves directly from tower A to tower C [self move:1 from:towerA to:towerC]; } else { [self hanoiWithDisk:diskNumber-1 towers:towerA :towerC :towerB];// Recursively move the plates numbered 1~diskNumber-1 on tower A to Tower B and tower C for assistance [self move:diskNumber from:towerA to:towerC];// Move the plate numbered diskNumber on tower A to tower C [self hanoiWithDisk:diskNumber-1 towers:towerB :towerA :towerC];// Recursively move the plates numbered 1~diskNumber-1 on Tower B to tower C, assisted by tower A } } // Moving process - (void)move:(NSInteger)diskIndex from:(OXTowerView *)fromTower to:(OXTowerView *)toTower { dispatch_semaphore_t sema = dispatch_semaphore_create(0);// The initialization semaphore is 0 NSLog(@"The first%ld Next move: move%ld No. 1 tray from tower%@Move to tower%@", ++self.moveCount, diskIndex, fromTower.towerId, toTower.towerId); for (OXDiskModel *disk in self.diskArray) { if (disk.index == diskIndex) { WeakSelf dispatch_async(dispatch_get_main_queue(), ^{// Switch back to the main thread for mobile animation [UIView animateWithDuration:1.0 animations:^{ StrongSelf if (strongSelf) { // Change the position of the plate CGPoint diskCenter = disk.center; NSInteger towerY = 10 + toTower.frame.origin.y; NSInteger towerHeight = toTower.frame.size.height-15; NSInteger diskHeight = towerHeight / strongSelf.diskNumber;// Height of each plate NSInteger hasDiskHieght = diskHeight * toTower.diskNumber;// Height of placed plates diskCenter.y = towerY + (towerHeight - hasDiskHieght) - diskHeight/2; disk.center = diskCenter; } } completion:^(BOOL finished) { if (finished) {// Animation complete StrongSelf if (strongSelf) { // Change the number of plates in fromTower fromTower.diskNumber--; // Change the number of plates in the tower toTower.diskNumber++; dispatch_semaphore_signal(sema);// Increase semaphore and end waiting } } }]; }); break; } } dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);// If the semaphore does not increase, wait until the animation is completed }
At this time, the operation can achieve the perfect effect:

junction
In order to solve the problem of blocking, some methods such as delayed execution and animation queue have been tried, but they are not as simple and effective as this method.
In the process of doing this, I used many small skills and optimized the code for many times. For myself, the code is more and more pleasing to the eye, which is really a good learning and training experience.
And it's interesting to watch the automatic animation problem-solving of Hanoi tower game, isn't it!
Example project: https://github.com/Cloudox/OXHanoiDemo