前言
最近寫了一個 iOS小游戲,純屬一時興起。動機:那天看到妹妹在朋友圈發了一組圖片,正好是九宮格的形狀,突然間就覺得這些圖片不就像是一個拼圖游戲嗎?如果可以直接移動玩拼圖,那也挺酷哇。擼起袖子就是干!做出來的效果就是這樣的:
基本思路
首先我選取了一張大的原始圖片,這張圖片用來裁成一定數量的小方塊(不用數學語言嚴謹描述了,影響閱讀性),最好是選取的圖片可以讓每個小方塊圖片都有一定的辨識度。原圖片右下角的一個小方塊丟棄作為可移動的空白空間。每一個小方塊都給她編上一個獨一無二的號碼。這個編號可以用來校驗拼圖是否完成。
方塊布局是使用UICollectionView來搭建的,難點在于拼圖的移動,實際上我是把圖塊的移動處理成了圖塊位置的交換,只要點擊你想要移動的圖塊,這個圖塊就會瞬移到空白位置,這樣來說在游戲體驗上移動更靈敏,效率更高!
開玩時,將圖塊順序打亂。
核心算法
判斷當前點擊的圖塊是否可移動
-(void)calculateIndexOfMoveable {
//記錄空白塊的索引,緊靠空白塊的方塊才可以移動,實際上就是與空白塊交換位置。初始化時的空白塊統一在右下角。
//計算當前可移動的方塊
// 白色塊所在行row = indexOfWhite / totalCols
// 白色塊所在列col = indexOfWhite % totalCols
left = indexOfWhite - 1
right = indexOfWhite + 1;
up = indexOfWhite - totalCols;
down = indexOfWhite + totalCols;
// 但是要排除一些四周情況下的索引
if ([self indexOfCol: left] > [self indexOfCol: indexOfWhite]) {
//left 排除
left = -1;
}
if ([self indexOfCol: right] < [self indexOfCol: indexOfWhite]) {
//right 排除
right = -1;
}
if (up < 0) {
//up 排除
up = -1;
}
if (down > totalCols*totalRows-1) {
//down 排除
down = -1;
}
}
-(NSInteger)indexOfRow:(NSInteger)index {
return index / totalCols;
}
-(NSInteger)indexOfCol:(NSInteger)index {
return index % totalCols;
}
上面的 calculateIndexOfMoveable方法可以優化成如下四個方法:
-(NSInteger)calculateIndexOfMoveable_left {
left = indexOfWhite - 1;
return [self indexOfCol: left] > [self indexOfCol: indexOfWhite] ? -1 : left;
}
-(NSInteger)calculateIndexOfMoveable_right {
right = indexOfWhite + 1;
return [self indexOfCol: right] < [self indexOfCol: indexOfWhite] ? -1 : right;
}
-(NSInteger)calculateIndexOfMoveable_up {
return (indexOfWhite - totalCols) < 0 ? -1 : indexOfWhite - totalCols;
}
-(NSInteger)calculateIndexOfMoveable_down {
return (indexOfWhite + totalCols) > (totalCols*totalRows-1) ? -1 : indexOfWhite + totalCols;
}
我這里定義了兩個數組,一個是圖片小方塊的數組,一個是圖片塊對應的編號數組。這兩個數組必須保持同步更新。也可以把圖片小方塊與其對應的編號作為一個模型類的屬性。也可以建立一個字典,將編號與圖片映射。
初始化圖片塊數組:
-(NSMutableArray *)dataSource {
if (!_dataSource) {
_dataSource = [NSMutableArray array];
CGFloat x,y,w,h;
w = (self.oringinalImg.image.size.width/totalCols)/[UIScreen mainScreen].scale;
h = (self.oringinalImg.image.size.height/totalRows)/[UIScreen mainScreen].scale;
for (int i=0; i<totalRows; i++) {
for (int j=0; j<totalCols; j++) {
x = j*w;
y = i*h;
CGRect rect = CGRectMake(x,y,w,h);
if ((i==totalRows-1) && (j== totalCols-1)) {
[_dataSource addObject: [[UIImage alloc] init] ];
} else {
[_dataSource addObject: [self ct_imageFromImage:self.oringinalImg.image inRect: rect]];
}
}
}
}
return _dataSource;
}
初始化圖片塊對應的編號數組:
-(NSMutableArray *)startIndexs {
if (!_startIndexs) {
_startIndexs = [NSMutableArray array];
for (int i = 0; i < totalCols*totalRows; i++) {
_startIndexs[i] = @(i);
};
}
return _startIndexs;
}
裁剪圖片的具體方法:
/**
* 從圖片中按指定的位置大小截取圖片的一部分
*
* @param image UIImage image 原始的圖片
* @param rect CGRect rect 要截取的區域
*
* @return UIImage
*/
- (UIImage *)ct_imageFromImage:(UIImage *)image inRect:(CGRect)rect {
//把像素rect 轉化為點rect(如無轉化則按原圖像素取部分圖片)
CGFloat scale = [UIScreen mainScreen].scale;
CGFloat x= rect.origin.x*scale,y=rect.origin.y*scale,w=rect.size.width*scale,h=rect.size.height*scale;
CGRect dianRect = CGRectMake(x, y, w, h);
//截取部分圖片并生成新圖片
CGImageRef sourceImageRef = [image CGImage];
CGImageRef newImageRef = CGImageCreateWithImageInRect(sourceImageRef, dianRect);
UIImage *newImage = [UIImage imageWithCGImage:newImageRef scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp];
return newImage;
}
兩個數組同步隨機亂序的方法,完成后兩個數組在相同的索引位置其對應關系仍保持不變。
- (void)randomArray {
//兩個數組同步打亂順序,早知道這么麻煩我就用模型將索引值綁定image了。/(ㄒoㄒ)/~~
NSMutableArray *newDatasourceArr = [NSMutableArray array];
NSMutableArray *newStartIndexArr = [NSMutableArray array];
int m = (int)self.dataSource.count;
for (int i=0; i<m; i++) {
int t = arc4random() % (self.dataSource.count);
newDatasourceArr[i] = self.dataSource[t];
newStartIndexArr[i] = self.startIndexs[t];
self.dataSource[t] = [self.dataSource lastObject];
self.startIndexs[t] = [self.startIndexs lastObject];
[self.dataSource removeLastObject];
[self.startIndexs removeLastObject];
}
self.dataSource = newDatasourceArr;
self.startIndexs = newStartIndexArr;
}
12.17修改更新:關于打亂圖序,我這種隨機打亂順序的做法欠妥,試玩幾次后發現有些情況我總是還原不了,回憶上學時玩過的一款游戲沒有出現過這樣的情況。這時候我開始懷疑并不是所有的序列都可以進行還原。而我卻忽略了,這非常不應該。
打亂后還需要驗證當前狀態是否有解。根據相關定理,如果打亂后的排列與原始排列的逆序數奇偶性相同,則是可還原的(證明比較簡單 參考鏈接——不可還原的拼圖)。如果拼圖的版塊是隨機打亂的,那么只有50%概率是可以被還原的。我這里統一將空格設置在末尾最后一個,可以忽略掉,不影響逆序數。
方案二:讓程序隨機移動數次,這樣肯定是能夠還原的。這個“數次”也值得商榷,要盡可能亂,又不能太多次了。但是我這個游戲設定的打亂后空格統一在最后一格,還需要調整空格位置,同樣用到剛才的逆序數相關定理,將空格與當前最后一個格子交換,現在排列奇偶性改變,還需要隨機將非空格的兩個格子進行交換一次。這樣就可以了。
方案三:對于m*n的拼圖,從拼圖板塊中任取三塊做輪換,通過[(m*n)/3]^2次輪換,即可實現相當“亂”的打亂效果。所謂三輪換,實質就是兩次交換:如123,1與2交換后,這時候狀態213,再3與2交換,這時候狀態312。體現在拼圖上很好實驗,把包含空格的2*2格子進行各種移動變換,就對應了3輪換。
還有個功能就是可以自定義幾行幾列,難點是需要動態更新相關數據,值得注意的是本例中cell是復用的,大小、內容需要根據需要即時調整。
最后奉獻上demo https://github.com/imsz5460/-puzzlegame 歡迎大家找bug,并提出優化意見,謝謝!