如何制作一款像超级玛丽兄弟一样基于平台的游戏-第二部分 (xcode,物理引擎,TMXTiledMap相关应用)

16 篇文章 0 订阅
8 篇文章 0 订阅

这篇文章还可以在这里找到 英语

接上篇

编译并运行!它是否正常运作了呢?是的!太好了!

何去何从? Where to Go From Here?

恭喜你!你已经构建了属于你自己的物理引擎了!如果你一步一步的跟着教程走到了这里,你可以深呼吸并锤锤后背了。这是本本游戏最难的一部分,在第2部分中将会是一马平川!

这里是到目前为止的完整的工程

第2部分中, 你将会让你的英雄考拉跑和跳。同时在地面增加一些危险物,并处理胜利/失败的逻辑。

如果你想获取平台游戏更多的信息,以下是我收集的一些资源:

你可以在留言区留言以让我知道你的进度!

第1部分完结

Learn how to make a game like Super Mario!

Learn how to make a game like Super Mario!

这是一篇IOS教程组的成员 Jacob Gundersen发布的教程, 他是一位独立游戏开发者,经营着Indie Ambitions 博客。去看看他最新的app吧Factor Samurai!

欢迎回到我们的两部分教程 – 如何制作一款像超级玛丽的游戏!

在 第1部分中,你学会了如何制作一个简单的,基于tile的物理引擎,使用这个引擎,你可以控制你的英雄考里奥在他的世界里有所作为。

在第2部分同时也是最后一部分中,你将学到如何控制考里奥跑和跳,这部分很有趣哟!

你还将加入一些具有碰撞的危险的地刺,处理胜利和失败,并毫无例外的加入一些免费的音效和音乐。

第2部分和第一部分相比较而言,简单多了,也短多了,这可是在第1部分中艰苦努力的奖励哦!重拾你的代码,享受之后的过程吧!

移动考里奥 Moving Koalio Around

你将要实现的控制系统相当简单。只有前进和跳跃,很像 1-bit Ninja。如果你点击屏幕的左半边,考里奥会前进,如果点击屏幕的右半边,考里奥就跳跃。

你没有听错,考里奥不能往回移动!真正的考拉是不会从危险中后退的。

因为考里奥不是由GameLevelLayer,而是由玩家控制向前移动的,你需要在Player类中实时更新它向前的素素。在Player类中加入如下属性(不要忘记synthesize部分!):

Player.h中:

@property (nonatomic, assign) BOOL forwardMarch;
@property (nonatomic, assign) BOOL mightAsWellJump;

Player.m中:

@synthesize forwardMarch = _forwardMarch, mightAsWellJump = _mightAsWellJump;

现在在GameLevelLayer中加入如下处理触摸事件的代码:

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {  
  for (UITouch *t in touches) {
    CGPoint touchLocation = [self convertTouchToNodeSpace:t];
    if (touchLocation.x > 240) {
      player.mightAsWellJump = YES;
    } else {
      player.forwardMarch = YES;
    }
  }
}
 
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  for (UITouch *t in touches) {
 
    CGPoint touchLocation = [self convertTouchToNodeSpace:t];
 
    //get previous touch and convert it to node space
    CGPoint previousTouchLocation = [t previousLocationInView:[t view]];
    CGSize screenSize = [[CCDirector sharedDirector] winSize];
    previousTouchLocation = ccp(previousTouchLocation.x, screenSize.height - previousTouchLocation.y);
 
    if (touchLocation.x > 240 && previousTouchLocation.x <= 240) {
      player.forwardMarch = NO;
      player.mightAsWellJump = YES;
    } else if (previousTouchLocation.x > 240 && touchLocation.x <=240) {
      player.forwardMarch = YES;
      player.mightAsWellJump = NO;
    }
  }
}
 
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 
  for (UITouch *t in touches) {
    CGPoint touchLocation = [self convertTouchToNodeSpace:t];
    if (touchLocation.x < 240) {
      player.forwardMarch = NO;
    } else {
      player.mightAsWellJump = NO;
    }
  }
}

这些内容相当直接。如果玩家点击坐标的x值小于240(一半的屏幕),你就把player中的forwardMarch变量置为YES。否则(点击坐标的x值大于240)就把mightAsWellJump变量置为YES。

touchesMoved稍微有一点复杂,因为你只想在触摸点穿越屏幕中心时才切换那些boolean值,所以你不得不把previousTouch坐标也计算在内。除了这点之外,你仅仅需要检测触摸点划过的方向来相应的设置那些boolean值。最后,如果玩家停止触摸屏幕的任何一侧时,你需要把相应的boolean值置为NO。

为了能够相应触摸事件,还需要做一些工作。首先,在init中加入这行:

	self.isTouchEnabled = YES;

然后,你还需要在AppDelegate.m打开多点触摸(为了检测玩家同时发出跑和跳的指令)。在[director_ pushScene: [GameLevelLayer scene]];之前加入以下内容:

	[glView setMultipleTouchEnabled:YES];

既然你把触摸点传递到了你的Player类中的一系列boolean变量中了,你便可以在update方法中加入一些代码,来让考里奥移动。首先先考虑前向移动,对Player.m中的update方法作如下修改:

-(void)update:(ccTime)dt {
    CGPoint gravity = ccp(0.0, -450.0);
    CGPoint gravityStep = ccpMult(gravity, dt);
 
    CGPoint forwardMove = ccp(800.0, 0.0);
    CGPoint forwardStep = ccpMult(forwardMove, dt); //1
 
    self.velocity = ccpAdd(self.velocity, gravityStep);
    self.velocity = ccp(self.velocity.x * 0.90, self.velocity.y); //2
 
    if (self.forwardMarch) {
        self.velocity = ccpAdd(self.velocity, forwardStep);
    } //3
 
    CGPoint minMovement = ccp(0.0, -450.0);
    CGPoint maxMovement = ccp(120.0, 250.0);
    self.velocity = ccpClamp(self.velocity, minMovement, maxMovement); //4
 
    CGPoint stepVelocity = ccpMult(self.velocity, dt);
 
    self.desiredPosition = ccpAdd(self.position, stepVelocity);
}

让我们一部分一部分解读它们:

  1. 当玩家点击屏幕时,你需要增加了一个向前的力。和以往一样,你把这个力根据时间戳(dt)缩减,这样就获得了平稳的加速。
  2. 这里你在水平方向上增加阻尼,以模拟摩擦力。你操作物理的方式跟之前的重力没什么两样。在每一帧中,向前的力都会被计算。

    当力被撤销时,你想要player停止,但不是立刻停止。这里你施加一个0.90的阻尼;换句话说,每帧
    里水平方向的速度减少百分之十。

  3. 在第3部分中,你检测是否触摸屏幕的变量,并根据情况施加forwardMove force。
  4. 在第4部分中,你应用了clamping。它限制了player最大的移动速度,包括水平方向的(跑的极限速度),向上的(跳跃速度)和向下的(下落速度)。

    这些damping和clamping值用来对游戏中事件发生频率设置上限。它还防止了你之前在第1部分中遇到过的速率过大的问题。

    你希望player需要大概1秒才到达最大的速度。这样做也能让player的移动更自然,并且可控。你允许的最大的力是正120,大概是1秒钟四分之一屏幕的距离。

    如果你想增加player的加速度,那么可以适当增加forwardMove,也可以适当改变目前为0.9的damping值。如果你想增加player的最大速度,那只需要简单地增加120那个值就行了。另外你也对跳跃250和下坠450的速度做了封顶,你同样也可以修改它们。

编译并运行。你将能够通过点击屏幕的左半边来让考里奥跑起来。看看它跑的样子多潇洒!

接下来,你将要赋予它跳跃的能力!

你的Mac将让它跳起来! Your Mac Will Make Him… Jump, Jump!

跳跃是平台游戏最主要的特色,也是此类游戏最大的乐趣来源。你肯定想把跳跃做的很流畅并且感觉很对。在本篇教学中,你将使用和刺猬索尼克同样的跳跃算法,在这里有详细说明。

update方法中if (self.forwardMarch) {行之前加入以下内容:

CGPoint jumpForce = ccp(0.0, 310.0);
 
if (self.mightAsWellJump && self.onGround) {
    self.velocity = ccpAdd(self.velocity, jumpForce);
}

如果你就到此为止的话(你可以编译并运行看看效果),你将会得到老学校里雅达利游戏机中的跳跃。每一次的跳跃都是同样的高度。你对player施加了一个力,并等待重力把它重新拉回来。

在现代的平台游戏中,玩家可以在对象跳跃的过程中对其进行操作。你想要可控的,完全不真实(但是乐趣十足)的马里奥兄弟/刺猬索尼克中使用的跳跃,在跳跃到半空中的时候还可以改变方向,甚至终止一次跳跃。

为了完成这个,你需要添加变量组件。有很多方法可以使用,不过你将会使用索尼克中的方法。对跳跃算法做一些修改,当玩家不再点击屏幕右侧时我们减弱向上的推力。把以上代码替换为一下内容:

  CGPoint jumpForce = ccp(0.0, 310.0);
  float jumpCutoff = 150.0;
 
  if (self.mightAsWellJump && self.onGround) {
    self.velocity = ccpAdd(self.velocity, jumpForce);
  } else if (!self.mightAsWellJump && self.velocity.y > jumpCutoff) {
    self.velocity = ccp(self.velocity.x, jumpCutoff);
  }

这些代码多做了一步。当玩家不再点击屏幕右侧时(self.mightAsWellJump为NO时),它检测player向上的速度,如果这个速度大于cutoff(150.0),那么就把player的速度设置为cutoff。

这样做能显著地减弱跳跃。通过这种方法,你总是可以得到一个最小的跳跃(至少和jumpCutoff一样高),但如果你持续按住屏幕,你将会得到一个完全的跳跃。

编译并运行。现在它看起来像个游戏了!从目前开始,你可能需要在真机上测试了(如果你之前没这样做过),这样你就可以同时触发两个“按钮”了(屏幕左和右)。

你的考里奥可以跑和跳了,但是最终他会跑出屏幕。是时候修正这个问题了!

基于tile的游戏教程中的以下代码片段加入到GameLevelLayer.m中:

-(void)setViewpointCenter:(CGPoint) position {
 
  CGSize winSize = [[CCDirector sharedDirector] winSize];
 
  int x = MAX(position.x, winSize.width / 2);
  int y = MAX(position.y, winSize.height / 2);
  x = MIN(x, (map.mapSize.width * map.tileSize.width) 
      - winSize.width / 2);
  y = MIN(y, (map.mapSize.height * map.tileSize.height) 
      - winSize.height/2);
  CGPoint actualPosition = ccp(x, y);
 
  CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
  CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
  map.position = viewPoint; 
}

这部分代码的作用是使player不会移动出屏幕。当考里奥在关卡边缘时,屏幕就不在追踪它了,并把关卡的边缘定位到屏幕的边缘。

这里对原版本的代码稍作了修改。在最后一行,我们用map移动代替了原先的layer移动。因为player是map的child,所以当player向右移动时,map向左移动,player一直会保持在屏幕的中心位置。

还有一点,touch方法需要layer的位置来做坐标转换。如果你移动了layer,你还需要另外把这些坐标计算进来。所以说我们的方法更简单。

想要完整的解释,请参考基于tile的游戏教程

你需要在update方法中添加如下内容:

	[self setViewpointCenter:player.position];

编译并运行。你可以控制考里奥穿梭在整个关卡中了!

失败之痛 The Agony of Defeat

现在你可以腾出手来处理胜利和失败的逻辑了。

先来处理失败的逻辑。关卡中有一些危险物。如果player碰到了它们,游戏就结束了。

因为它们是固定的tile,你需要像处理wall的碰撞一样处理它们。不同的是,你需要在碰撞发生时结束,而不是处理碰撞。到此为止,你已经在离最终完成不远了,还有一些剩余事项需要处理。

GameLevelLayer.m中加入如下方法:

-(void)handleHazardCollisions:(Player *)p {
  NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:hazards ];
  for (NSDictionary *dic in tiles) {
    CGRect tileRect = CGRectMake([[dic objectForKey:@"x"] floatValue], [[dic objectForKey:@"y"] floatValue], map.tileSize.width, map.tileSize.height);
    CGRect pRect = [p collisionBoundingBox];
 
    if ([[dic objectForKey:@"gid"] intValue] && CGRectIntersectsRect(pRect, tileRect)) {
      [self gameOver:0];
    }
  }
}

以上这些代码看起来很眼熟,因为它是从checkAndResolveCollisions方法中拷贝过来的。唯一的一个新的方法是gameOver。此方法有一个参数,0代表player失败,1代表player胜利。

由于你使用了hazards层代替了wall层,你需要在文件一开头的@interface块中加入一个成员变量CCTMXLayer *hazards;,在init中初始化它(在初始化wall之后的一行):

	hazards = [map layerNamed:@"hazards"];

最后一件事是你需要在update方法中调用这个方法:

-(void)update:(ccTime)dt {
  [player update:dt];
 
  [self handleHazardCollisions:player];
  [self checkForAndResolveCollisions:player];
  [self setViewpointCenter:player.position];
}

现在,如果player跑到了一个危险物层的tile上,你就会调用gameOver。那个方法会弹出失败信息和一个重新开始的按钮(当然也可以是胜利的):

-(void)gameOver:(BOOL)won {
	gameOver = YES;
	NSString *gameText;
 
	if (won) {
		gameText = @"You Won!";
	} else {
		gameText = @"You have Died!";
	}
 
  CCLabelTTF *diedLabel = [[CCLabelTTF alloc] initWithString:gameText fontName:@"Marker Felt" fontSize:40];
  diedLabel.position = ccp(240, 200);
  CCMoveBy *slideIn = [[CCMoveBy alloc] initWithDuration:1.0 position:ccp(0, 250)];
  CCMenuItemImage *replay = [[CCMenuItemImage alloc] initWithNormalImage:@"replay.png" selectedImage:@"replay.png" disabledImage:@"replay.png" block:^(id sender) {
    [[CCDirector sharedDirector] replaceScene:[GameLevelLayer scene]];
  }];
 
  NSArray *menuItems = [NSArray arrayWithObject:replay];
  CCMenu *menu = [[CCMenu alloc] initWithArray:menuItems];
  menu.position = ccp(240, -100);
 
  [self addChild:menu];
  [self addChild:diedLabel];
 
  [menu runAction:slideIn];
}

第一行初始化了一个新的叫做gameOver的boolean变量。你使用它来停止update方法,这样可以组织player继续移动并和关卡产生互动。你马上就会遇到使用它的地方。

接下来的代码创建了一个label,并根据玩家胜利或失败为赋值一个字符串。它还创建了一个重新开始的按钮。

这些CCMenu里基于block的方法真心好用。在此处,我们使用CCDirector的replaceScene方法复制一个同样的场景来重新开始关卡。你还使用了CCAction,CCMoveBy来让replay按钮动态进入场景,没别的目的,只是为了好玩儿。

最后需要做的是把gameOver变量加入到GameLevelLayer类中。把它作为成员变量就可以了,因为你并不需要从其他类访问它。在GameLevelLayer.m一开头的@interface块中加入以下内容:

CCTMXLayer *hazards;
BOOL gameOver;

并如下修改update方法:

-(void)update:(ccTime)dt {
  if (gameOver) {
    return;
  }
  [player update:dt];
  [self checkForAndResolveCollisions:player];
  [self handleHazardCollisions:player];
  [self setViewpointCenter:player.position];
}

再次编译并运行,找到一些地刺跳上去!你会发现如下所示的场景:

这个步骤别重复太多次,否则动物保护组织会找上你的! :]

地狱深处 The Pit of Doom

现在来处理当考里奥掉落的情况。当发生这种情况时,你将结束游戏。

目前的代码逻辑是当发生掉落时,程序就挂掉了,错误是TMXLayer: invalid position error。这里需要你解决一下。这个情况发生的是在getSurroundingTilesAtPosition:方法中调用tileGIDAt:的位置。

GameLevelLayer.m中的getSurroundingTilesAtPosition:方法里,tileGIDat:行之前加入以下代码:

if (tilePos.y > (map.mapSize.height - 1)) {
    //fallen in a hole
    [self gameOver:0];
    return nil;
}

这些代码会执行gameOver并停止继续构建tile数组。你还需要在checkForAndResolveCollisions中阻止循环遍历tile的过程。在NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:walls ];行之后,加入以下代码块儿:

  if (gameOver) {
    return;
  }

你将会中止循环,以此来避免试图遍历不完整的数组导致的程序崩溃。

编译并运行。找一个坑掉进去,看,现在不会挂了!游戏正常的进入gameover界面了。

胜利! Winning!

现在来处理当英雄考里奥赢得游戏的情景!

所有需要做的就是检测player的X坐标是否触发了胜利条件,当它穿过关卡最右端时,就胜利了。目前的关卡大约有3400像素宽。当player到达3130像素的时候,你就认为游戏胜利了。

GameLevelLayer.m中加入以下新方法:

-(void)checkForWin {
  if (player.position.x > 3130.0) {
    [self gameOver:1];
  }
}
-(void)update:(ccTime)dt {
  [player update:dt];
 
  [self handleHazardCollisions:player];
  [self checkForWin];
  [self checkForAndResolveCollisions:player];
  [self setViewpointCenter:player.position];
}

编译并运行。控制你的英雄考里奥穿越整个关卡,如果你能让它到达结束点,你将会达到这些信息:

免费的音乐和音效 Gratuitous Music and Sound Effects

是时候加些免费的音乐和音效了!

我们这就开始。在GameLevelLayer.m 和 Player.m的开头加入以下内容:

#import "SimpleAudioEngine.h"

然后在GameLevelLayer中的init方法加入下边一行代码:

	[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"level1.mp3"];

这个能带来一些不错的游戏音乐。感谢Incompetech.com的Kevin Macleod创做的音乐(Brittle Reel)。他有很多的CC licensed(非商业使用)的音乐!

现在来加入一个跳跃的音效。在Player.m加入以下内容到update方法中跳跃的部分:

if (self.mightAsWellJump && self.onGround) {
    self.velocity = ccpAdd(self.velocity, jumpForce);
    [[SimpleAudioEngine sharedEngine] playEffect:@"jump.wav"];
} else if (!self.mightAsWellJump && self.velocity.y > jumpCutoff) {
    self.velocity = ccp(self.velocity.x, jumpCutoff);
}

最后,当考里奥掉进一个坑或者碰到危险物时,播放一个音效。在GameLevelLayer.m中的gameOver方法中做这件事:

-(void)gameOver {
  gameOver = YES;
  [[SimpleAudioEngine sharedEngine] playEffect:@"hurt.wav"];  
  CCLabelTTF *diedLabel = [[CCLabelTTF alloc] initWithString:@"You have Died!" fontName:@"Marker Felt" fontSize:40];
  diedLabel.position = ccp(240, 200);

编译并运行,享受属于你的美妙的旋律吧。

到这里就全部结束了,你已经完成了一个平台游戏。你.真的.很棒!

何去何从? Where to Go From Here?

这里是最终工程的源代码下载地址。

其实还有很多内容没有但是可以被包含进来的:从敌人的碰撞和AI,到移动能力的加强(爬墙,双跳,等等),再到关卡设计指导。

说到这个,有个好消息给你!

平台游戏Starter Kit

我非常高兴地宣布所有以上这些内容甚至更多的内容都会被包含进即将到来的平台游戏Starter Kit!这里有一个关于它的预览视频:

以下这些是你能从这个Starter Kit学到的知识点:

  • 如何管理和读取多个关卡
  • 如何制作一个可滑动的,带解锁功能的选关界面
  • 如何在Cocos2D中集成UIKit Storyboards
  • 如何高效的使用sprite sheets, animations, tilesets,并使用像素图片!
  • 如何创建一个状态机来处理角色/敌人的动画和行为
  • 更多的关于如何制作令人惊奇和游戏的基于tile的物理引擎!
  • 如何创建一个在屏幕之上的虚拟手柄和HUD
  • 如何添加iCade支持
  • 平台游戏的关卡设计
  • 如何构建敌人的AI和动态行为。
  • 如何添加一个EPIC Boss(隐藏boss)战斗!
  • 数个顶尖的IOS平台游戏开发者的采访,分享经验和技巧
  • . . . 还有很多,很多!

如果你对平台游戏Starter Kit感兴趣,请确保你已经订阅了Ray’s monthly newsletter,我将会在那里发布有关它的消息! :]

同时,不要忘记在第1部分中末尾推荐的那些资源。

我希望你享受制作你自己的物理引擎的过程并制作你自己的平台游戏!

这是一篇IOS教程组的成员 Jacob Gundersen发布的教程, 他是一位独立游戏开发者,经营着Indie Ambitions 博客。去看看他最新的app吧Factor Samurai!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值