童年的記憶——iOS 拼圖游戲

前言

最近寫了一個 iOS小游戲,純屬一時興起。動機:那天看到妹妹在朋友圈發了一組圖片,正好是九宮格的形狀,突然間就覺得這些圖片不就像是一個拼圖游戲嗎?如果可以直接移動玩拼圖,那也挺酷哇。擼起袖子就是干!做出來的效果就是這樣的:

demo.gif

基本思路

首先我選取了一張大的原始圖片,這張圖片用來裁成一定數量的小方塊(不用數學語言嚴謹描述了,影響閱讀性),最好是選取的圖片可以讓每個小方塊圖片都有一定的辨識度。原圖片右下角的一個小方塊丟棄作為可移動的空白空間。每一個小方塊都給她編上一個獨一無二的號碼。這個編號可以用來校驗拼圖是否完成。

方塊布局是使用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,并提出優化意見,謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容