【游戏系统介绍】
这系列文章记录一个完整的小游戏APP实现过程。游戏“HardToReach”是对早期FlappyBird的复刻学习,在基础功能上添加了游戏道具、游戏关卡、游戏介绍、丰富的音乐和UI资源、加入了账号功能与排行榜。游戏整体系统使用B/S架构,服务端是在云服务器上运行的Python脚本,完成客户端网络请求与数据库数据交互逻辑处理。游戏本身客户端使用了双层MVC嵌套的结构,实现上用到iOS在2014年新发布的原生引擎SpriteKit,在丰富的物理世界中构建游戏组件并在上层横向构建其他VC搭建MVC。
【游戏效果展示】
在开始游戏实现说明前,先看一下最终实现的效果。
(由于平台不支持上传视频,原60帧高清视频压缩为GIF,实际是远好于动图效果的)
1. 主菜单功能展示如下
2. 游戏功能展示如下
【平台技术介绍】
- Xcode:苹果公司开发的Mac OS X上的集成开发工具(IDE)
- Objective-C:基于C的前提加入了面向对象扩充而成的编程语言用于编写iOS应用程序等。
- MVC:是Model-View- Controller的简写,即模型-视图-控制器。搭建MVC的用途就是把M和V的代码分开以减少V层代码臃肿问题。
- SpriteKit引擎:iOS7与后来的系统版本中原生内置的新框架。该框架主要用来开发2D游戏。目前已经支持的内容包括精灵,各种特效,集成了物理引擎库等许多内容。
【游戏引擎SpriteKit基础工作】
- 首先将我们熟知某个的UIViewController作为根视图引出游戏内容,与常规iOS应用不同的是其视图类型为SKView而非UIView。
- SKView是UIView的子类,用于执行场景的呈现。
- 所谓场景是SKScene类-用于表达画面、动画和渲染游戏内容的构建。其生命周期如下图所示,表现出如何渲染每一帧的过程。
- SpriteKit框架下的节点大都是SKNode为基础类型进行子类扩展而生的。系统自带的包括SKSpriteNode、SKShapeNode、SKLabelNode、SKVideoNode等。
- SpriteKit框架下依然存在许多委托方法,可以在协议中去实现它们来达成一些事件的处理。例如SKSceneDelegate、SKPhysicsContactDelegate。
- 使用SKAction来达成SKNode对象的执行方法,使用SKTranstion来实现SKScene场景之间的转换。SKTexture实现内容的纹理外观。
以上不难看出,游戏内容是合理使用丰富的类和自定义一些子类来构建出具体的场景,再把场景加入到视图控制器下的视图进行呈现,不同场景之间的切换也是在同一个视图控制器下。(SKNode与其他类型——SKScene——SKView——UIViewController)
【游戏APP的项目结构】
- ViewController:包括管理菜单的根视图器与登录注册,排行榜等。
- View:包括背景视图,Toast提示类,Loading类等自定义类。
- Nodes:充当Model的类,包括障碍物,世界,道具,人物类以及常量接口文件。
- Scenes:游戏内容的主体场景,包括菜单场景,游戏场景,结束场景。
- 资源文件:Assets.xcassets中存放的图片资源,Sounds文件夹下的各种音频资源。
个人认为,在整个APP中,SKScene一边对于游戏部分扮演着呈现画面的VC角色,一边扮演着对于整个APP来说的Model角色,因此可以看作双层的MVC结构。
游戏内容的MVC关系如下:
将整个游戏场景看作APP的Model,上层的MVC关系如下:
【主菜单实现】
LJZMenuScene(SKScene子类)——SKView(UIView子类)——GameViewController(UIViewController子类)
- GameViewController作为APP的根视图控制器,其SKView呈现菜单场景,作为视图控制器依然可以加入UIKit的组件,用4个UIButton类型来控制菜单功能(账号管理、开始游戏、游戏介绍、排行榜)GameViewController.h如下:
#import <UIKit/UIKit.h>
#import <SpriteKit/SpriteKit.h>
#import <GameplayKit/GameplayKit.h>
@interface GameViewController : UIViewController
@property (nonatomic, strong)UIButton *LoginButton;
@property (nonatomic, strong)UIButton *aboutButton;
@property (nonatomic, strong)UIButton *rankButton;
@property (nonatomic, strong)UIButton *startButton;
@end
- 完成生命周期方法以及UI初始化,以开始游戏按钮为例,使用自己画的img初始化并实现点击处理。GameViewController.m部分内容如下:
- (void)viewDidLoad {
[super viewDidLoad];
[self SetUpGameView];
[self SetUpButton];
[self SetNotificationObserver];
}
- (void)SetUpGameView
{
SKView *skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
SKScene *scene = [LJZMenuScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
[skView presentScene:scene];
}
- (void)SetUpButton
{
[self CreateLoginButton];
[self CreateStartButton];
[self CreateAboutButton];
[self CreateRankButton];
}
- (void)CreateStartButton
{
self.startButton = [UIButton buttonWithType:UIButtonTypeCustom];
_startButton.frame = CGRectMake(0, 0, 150, 50);
_startButton.center = CGPointMake(CGRectGetMidX(self.view.frame), CGRectGetMidY(self.view.frame) * 1.2);
_startButton.layer.shadowOffset = CGSizeMake(10, 10);
_startButton.layer.shadowOpacity = 0.5;
_startButton.layer.shadowColor = [UIColor blackColor].CGColor;
[_startButton setBackgroundImage:[UIImage imageNamed:@"button_start"] forState:UIControlStateNormal];
[_startButton addTarget:self action:@selector(StartButtonClicked) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_startButton];
}
- 菜单场景初始化,场景内容在UIKit控件的下层,作为一个动态的背景加入到根视图中,由展示可看到,菜单场景包括了标题、背景世界、地板、角色(其实还有BGM)。与UIViewController中的viewDidLoad:方法类似,在didMoveToView:中完成一个场景的初始化。使用AVAudioPlayer来管理背景音乐、SKTexture来初始化图片资源完成背景世界的创建、使用NSNotification来处理根视图发来的事件(这里指开始游戏)。SKLabelNode类是用来实现游戏大标题的。细心不难发现这里开始游戏时也发出了一个通知,这个是由GameViewController来接受的,用途的隐藏UIKit控件,否则在根视图切换场景时这些控件将一直呈现在视图顶层(而实际上我们只需要它们停留在菜单时出现)。
#import "LJZMenuScene.h"
#import "LJZGameScene.h"
#import "LJZTerrain.h"
#import "LJZHero.h"
#import <AVFoundation/AVFoundation.h>
@interface LJZMenuScene ()
@property (nonatomic, strong) AVAudioPlayer *bgmPlayer;
@property (nonatomic, strong) SKLabelNode *titleLabel;
@end
@implementation LJZMenuScene
#pragma mark - Life Cycle
- (id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size]) {
}
return self;
}
- (void)didMoveToView:(SKView *)view
{
[super didMoveToView:view];
[self ApplyMusic];
[self setup];
[self SetNotificationObserver];
}
#pragma mark - Init
- (void)ApplyMusic
{
NSString *bgmPath = [[NSBundle mainBundle] pathForResource:@"menu_bgm" ofType:@"mp3"];
self.bgmPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:bgmPath] error:NULL];
self.bgmPlayer.numberOfLoops = -1;
[self.bgmPlayer play];
}
- (void)setup
{
[self createWorld];
[self createHero];
[LJZTerrain addNewNodeTo:self withType:1];
[self SetUpTitleLabel];
}
- (void)SetNotificationObserver
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(StartToPlayGame) name:@"needToPlayGame" object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:@"needToShowButton" object:nil];
}
#pragma mark - SetUp UI
- (void)createWorld
{
SKTexture *backgroundTexture = [SKTexture textureWithImageNamed:@"background"];
SKSpriteNode *background = [SKSpriteNode spriteNodeWithTexture:backgroundTexture size:self.view.frame.size];
background.position = (CGPoint) {CGRectGetMidX(self.view.frame), CGRectGetMidY(self.view.frame)};
[self addChild:background];
self.scaleMode = SKSceneScaleModeAspectFit;
}
- (void)createHero
{
SKSpriteNode *hero = [LJZHero createSpriteOn:self];
hero.position = (CGPoint) {CGRectGetMidX(self.view.frame), CGRectGetMidY(self.view.frame)};;
}
- (void)SetUpTitleLabel
{
self.titleLabel = [SKLabelNode labelNodeWithFontNamed:@"GillSans-UltraBold"];
_titleLabel.text = @"Hard To Reach";
_titleLabel.fontSize = 40;
_titleLabel.fontColor = [SKColor brownColor];
_titleLabel.position = CGPointMake(CGRectGetMidX(self.frame),
CGRectGetMidY(self.frame) + 250);
[self addChild:_titleLabel];
}
#pragma mark - Handle event
- (void)StartToPlayGame
{
[self.bgmPlayer stop];
self.bgmPlayer = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:@"needToHideButton" object:nil];
SKTransition *reveal = [SKTransition fadeWithDuration:.5f];
LJZGameScene *newScene = [[LJZGameScene alloc] initWithSize: self.size];
[self.scene.view presentScene: newScene transition: reveal];
}
@end
- 以上可以看到关于角色、地板是由其他类来创建的,除此外还包括了障碍物与游戏道具等游戏组件,这些是抽象出的Model类用来方便在各个场景中管理,在后面的内容我会详细说明。
本篇主要讲述了SpriteKit引擎开发小游戏的总体结构与主视图实现,下一篇我将展示主菜单中账号功能、排行榜、以及游戏组件的内容。