ダンジョン型ゲームの基礎(壁との当たり判定)【Xcode5, iOS7】
かーなーりー久しぶりのコード掲載です。
以前に壁の当たり判定についてコメントをいただき、時間ができたら記事書きますと言ったもののずーっと時間ができないまま今に至っており気になってました。ただ、やっぱり詳しい解説を書く時間は無いので、がんばって以下のコードを読み解いてマスターして欲しいと切に願います。
仕様は次のとおり。
・非スプライトキットです。本当はスプライトキットで書き直ししたかったのですが取り急ぎ過去資産そのままで。もう私はiPhoneでゲームつくるにはスプライトキット必須と思うようになりました。(iPhone6画面対応を考えると)
・キャラのサイズはいつもより小さく16x16。
・迷路は自動生成です。詳細は「棒倒し法」をGoogle先生に尋ねてください。
・3つのアミュレットをゲットすると出口が出現します。
・1面クリアすると、迷路が大きくなります。どんどん大きくなります。
・最初は迷路は1画面に収まっていますが、画面より大きくなってもスクロール対応しています。
・画面描画のための座標と、ワールド座標の2種類を保持しているのがキモです。ここの理解をマスターすると一画面以上のスクロールゲームが作れるようになります。(スーパーマリオ、ドラクエ)
・迷路はどんどんデカくなる一方なので、ある程度大きくなるとものスゴク描画が遅くなります。これは見えない部分もすべて描画しているため。(スプライトキット使うと、見えない部分の描画は自動で省略されるのでこんなことにはならない。これもスプライトキットを推奨する理由の一つ。)
・ビルドはXcode5.1.1で確認。
ではコード掲載。今までのとおりView.hとView.mの2ファイルを掲載します。読み込んでいるpngファイル、AppDelegate, ViewControllerは省略。
DungeonDiverView.h
// // DungeonDiverView.h // DungeonDiver // // Created by 中道 忠和 on 2014/06/08. // Copyright (c) 2014年 Tadakazu Nakamichi. All rights reserved. // #import <UIKit/UIKit.h> #define BN 500 #define GN 5 @interface DungeonDiverView : UIView { int WIDTH; int HEIGHT; CGContextRef context; NSMutableArray* draw; CGPoint lastPoint; int touch_direction; // 0:stop 1:up 2:right 3:down 4:left float tx; //タッチx座標記憶用 float ty; int gs; //game status int stage; struct prayer { int x; int y; int wx; int wy; int vx; int vy; }; struct prayer p; int hp; int str; int def; int MAP[109][109]; int mx; // MAP x int my; // MAP y int mx2; // MAP x2 int my2; // MAP y2 int mapx; int mapy; int mapx2; int mapy2; int maptopx; //マップ読み込み左上インデックス int maptopy; //マップ読み込み左上インデックス int mapxcounter; int mapycounter; int maxx; int maxy; struct amulet { int x; int y; int visible; int color; }; struct amulet a[3]; int amuletcounter; int exitflag; struct exit { int x; int y; int visible; }; struct exit e; } @end
DungeonDiverView.m
// // DungeonDiverView.m // DungeonDiver // // Created by 中道 忠和 on 2014/06/08. // Copyright (c) 2014年 Tadakazu Nakamichi. All rights reserved. // #import "DungeonDiverView.h" @implementation DungeonDiverView - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialization code } return self; } -(id)initWithCoder:(NSCoder*)coder { self=[super initWithCoder:coder]; if (self) { CGRect bounds=[[UIScreen mainScreen] bounds]; WIDTH = bounds.size.width; HEIGHT= bounds.size.height; // 画面の準備 context=NULL; // キャラクターの絵を準備 draw=[NSMutableArray array]; //配列を設定 //MAP //STAGE01 [ draw addObject:[UIImage imageNamed:@"fighter.png"]]; //0 [ draw addObject:[UIImage imageNamed:@"block01.png"]]; //1 [ draw addObject:[UIImage imageNamed:@"redamulet.png"]]; //2 [ draw addObject:[UIImage imageNamed:@"greenamulet.png"]]; //3 [ draw addObject:[UIImage imageNamed:@"blueamulet.png"]]; //4 [ draw addObject:[UIImage imageNamed:@"exit.png"]]; //5 //画面再描画タイマーの準備 [NSTimer scheduledTimerWithTimeInterval:0.01f target:self selector:@selector(onTick:) userInfo:nil repeats:YES]; //戦士の準備 [self setPlayer]; hp=10; str=1; def=1; // その他変数 初期設定 mx=0; my=0; mx2=0; my2=0; touch_direction=0; stage=0; gs=1; //MAP初期化 [self makeMap]; } return self; } - (void)drawRect:(CGRect)rect { // Drawing code // 画面の準備(まあ、呪文のようなものです。アブダカダブラ。) if (context!=NULL) { CGContextRelease(context); context=NULL; } context=UIGraphicsGetCurrentContext(); CGContextRetain(context); // 画面を真っ黒に塗って、全部消す。(それからあとで背景とキャラクターを高速で描くから、アニメーションに見える) CGContextSetRGBFillColor(context,0,0,0,1); CGContextSetRGBStrokeColor(context,0,0,0,1); CGContextFillRect(context,CGRectMake(0,0,WIDTH,HEIGHT)); if (gs==1) { //通常ゲーム //ステージ描画 [self drawMap]; //アミュレット描画 [self drawAmulet]; //出口描画 [self drawExit]; //プレイヤー描画 [self drawPlayer]; //パラメーター描画 [self drawParameters]; } } //プレイヤー初期設定 -(void)setPlayer { p.x=64; //最初のX座標 p.y=80; //最初のY座標 p.wx=p.x; //ワールドX座標 p.wy=p.y; //ワールドY座標 p.vx=0; //最初のX座標の移動量 p.vy=0; //最初のY座標の移動量 } -(void)drawPlayer { [[draw objectAtIndex:0] drawAtPoint:(CGPointMake(p.x,p.y))]; // 移動処理 // タッチ操作の方向に応じて移動量を代入しておく if (touch_direction==0) {p.vx=0; p.vy=0;} if (touch_direction==1) {p.vy=-1;} if (touch_direction==2) {p.vx= 1;} if (touch_direction==3) {p.vy= 1;} if (touch_direction==4) {p.vx=-1;} // カベ判定 // x if (p.vx>0) { mx =(p.wx+15+p.vx)/16; my =p.wy/16; my2=(p.wy+15)/16; if (MAP[my ][mx]!=0) { p.vx=0; } if (MAP[my2][mx]!=0) { p.vx=0.0f; } } else { mx =(p.wx+p.vx)/16; my =p.wy/16; my2=(p.wy+15)/16; if (MAP[my ][mx]!=0) { p.vx=0; } if (MAP[my2][mx]!=0) { p.vx=0; } } //マップスクロール処理 p.x=p.x+p.vx; if (p.x<64.0f) { p.x=64.0f; mapx++; if (mapx>64.0f) mapx=64.0f; } if (p.x>224.0f) { p.x=224.0f; mapx--; if (mapx<-1584.0f) mapx=-1584.0f; } p.wx=p.wx+p.vx; // y if (p.vy>0) { mx =p.wx/16; mx2=(p.wx+15)/16; my =(p.wy+15+p.vy)/16; if (MAP[my][mx ]!=0) { p.vy=0; } if (MAP[my][mx2]!=0) { p.vy=0; } } else { mx =p.wx/16; mx2=(p.wx+15)/16; my =(p.wy+p.vy)/16; if (MAP[my][mx ]!=0) { p.vy=0; } if (MAP[my][mx2]!=0) { p.vy=0; } } //マップスクロール処理 p.y=p.y+p.vy; if (p.y<64.0f) { p.y=64.0f; mapy++; if (mapy>64.0f) mapy=64.0f; } if (p.y>416.0f) { p.y=416.0f; mapy--; if (mapy<-1584.0f) mapy=-1584.0f; } p.wy=p.wy+p.vy; } // タイマー -(void)onTick:(NSTimer*)timer { [self setNeedsDisplay]; } // タッチパネル操作 -(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { lastPoint = [[touches anyObject] locationInView:self.superview]; } -(void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { // タッチした座標を取得 CGPoint point=[[touches anyObject] locationInView:self.superview]; // タッチ操作量はX方向とY方向のどちらが多いか絶対値で比較する(lastPointとpの座標を取得する構造がゆえに) float temp_vx, temp_vy; temp_vx=abs(lastPoint.x-point.x); temp_vy=abs(lastPoint.y-point.y); if (temp_vx > temp_vy) { if (lastPoint.x-point.x<0) { touch_direction=2; } else { touch_direction=4; } } else { if (lastPoint.y-point.y<0) { touch_direction=3; } else { touch_direction=1; } } //tx,ty tx=lastPoint.x; ty=lastPoint.y; // さっき取得したタッチの座標をラストポイントに書き換えておく。 lastPoint = point; } -(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { tx=lastPoint.x; ty=lastPoint.y; } -(void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { [self touchesEnded:touches withEvent:event]; } //マップ描画 -(void)drawMap { for (int j=0;j<109; j++) { for (int i=0; i<109; i++) { int mapid=MAP[j][i]; if (mapid==1) [[draw objectAtIndex:1] drawAtPoint:(CGPointMake(i*16+mapx,j*16+mapy))]; } } } //アミュレット描画 -(void)drawAmulet { for (int i=0;i<3;i++) { if (a[i].visible==1) { [[draw objectAtIndex:a[i].color+2] drawAtPoint:(CGPointMake(a[i].x+mapx,a[i].y+mapy))]; //当たり判定 if (((p.wx+7)-(a[i].x+7))*((p.wx+7)-(a[i].x+7))+((p.wy+7)-(a[i].y+7))*((p.wy+7)-(a[i].y+7)) < 400) { a[i].visible=0; amuletcounter++; if (amuletcounter==3) e.visible=1; } } } } //出口描画 -(void)drawExit { if (e.visible==1) { [[draw objectAtIndex:5] drawAtPoint:(CGPointMake(e.x+mapx,e.y+mapy))]; //当たり判定 if (((p.wx+7)-(e.x+7))*((p.wx+7)-(e.x+7))+((p.wy+7)-(e.y+7))*((p.wy+7)-(e.y+7)) < 400) { e.visible=0; stage++; if (stage>50) stage=50; [self makeMap]; [self setPlayer]; } } } //パラメーター描画 -(void)drawParameters { UIFont* font1 = [UIFont fontWithName:@"Copperplate" size:18]; NSString* s1=[NSString stringWithFormat:@"DUNGEON %02d",stage+1]; NSString* s2=[NSString stringWithFormat:@"HP %08d",hp]; NSString* s3=[NSString stringWithFormat:@"STR %08d",str]; NSString* s4=[NSString stringWithFormat:@"DEF %08d",def]; [s1 drawAtPoint:CGPointMake(0,400) withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:font1, NSFontAttributeName,[UIColor whiteColor],NSForegroundColorAttributeName,nil]]; [s2 drawAtPoint:CGPointMake(0,420) withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:font1, NSFontAttributeName,[UIColor whiteColor],NSForegroundColorAttributeName,nil]]; [s3 drawAtPoint:CGPointMake(0,440) withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:font1, NSFontAttributeName,[UIColor whiteColor],NSForegroundColorAttributeName,nil]]; [s4 drawAtPoint:CGPointMake(0,460) withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:font1, NSFontAttributeName,[UIColor whiteColor],NSForegroundColorAttributeName,nil]]; } //マップ自動生成 -(void)makeMap { // ステージ //最大値計算 maxx=stage*2+9; maxy=stage*2+9; uint8_t map[maxy][maxx]; //ステージ初期化1 for (int j=0;j<maxy;j++) { for (int i=0;i<maxx;i++) { map[j][i]=0; } } //ステージ初期化2 外壁 //縦外壁 for (int j=0;j<maxy;j++) { map[j][0]=1; map[j][maxx-1]=1; } //横外壁 for (int i=0;i<maxx;i++) { map[0][i]=1; map[maxy-1][i]=1; } //ステージ初期化3 2列目縦横に柱配置 for (int j=2;j<maxy;j=j+2) { for (int i=2;i<maxx;i=i+2) { map[j][i]=1; } } //棒倒し法 srand((unsigned)time(NULL)); int r; for (int j=2;j<maxy-2;j=j+2) { for (int i=2;i<maxx-1;i=i+2) { if (j==2) { r=rand()%4; } else { r=rand()%4+1; } if (r==0) { if (map[j-1][i]==0) map[j-1][i]=1; } if (r==1) { if (map[j][i+1]==0) map[j][i+1]=1; } if (r==2) { if (map[j+1][i]==0) map[j+1][i]=1; } if (r==3) { if (map[j][i-1]==0) map[j][i-1]=1; } } } //グローバル変数に読み込ませる for (int j=0;j<maxy;j++) { for (int i=0;i<maxx;i++) { MAP[j][i]=map[j][i]; } } //map描画変数初期化 mapx=0; mapy=0; //amulet初期化 for (int i=0;i<3;i++) { a[i].color=i; do { a[i].x=(rand()%(maxx-2)+1)*16; a[i].y=(rand()%(maxy-2)+1)*16; } while (map[a[i].y/16][a[i].x/16]!=0); a[i].visible=1; } amuletcounter=0; //exit初期化 exitflag=0; do { e.x=(rand()%(maxx-2)+1)*16; e.y=(rand()%(maxy-2)+1)*16; } while (map[e.y/16][e.x/16]!=0); e.visible=0; } @end