前言
最近写了一个 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,并提出优化意见,谢谢!