【完整APP】SpriteKit引擎開發iOS小遊戲之初見

【遊戲系統介紹】

這系列文章記錄一個完整的小遊戲APP實現過程。遊戲“HardToReach”是對早期FlappyBird的復刻學習,在基礎功能上添加了遊戲道具、遊戲關卡、遊戲介紹、豐富的音樂和UI資源、加入了賬號功能與排行榜。遊戲整體系統使用B/S架構,服務端是在雲服務器上運行的Python腳本,完成客戶端網絡請求與數據庫數據交互邏輯處理。遊戲本身客戶端使用了雙層MVC嵌套的結構,實現上用到iOS在2014年新發布的原生引擎SpriteKit,在豐富的物理世界中構建遊戲組件並在上層橫向構建其他VC搭建MVC。

【遊戲效果展示】

在開始遊戲實現說明前,先看一下最終實現的效果。
(由於平臺不支持上傳視頻,原60幀高清視頻壓縮爲GIF,實際是遠好於動圖效果的)
1. 主菜單功能展示如下
主菜單功能展示
2. 遊戲功能展示如下
遊戲功能展示

【平臺技術介紹】

  1. Xcode:蘋果公司開發的Mac OS X上的集成開發工具(IDE)
  2. Objective-C:基於C的前提加入了面向對象擴充而成的編程語言用於編寫iOS應用程序等。
  3. MVC:是Model-View- Controller的簡寫,即模型-視圖-控制器。搭建MVC的用途就是把M和V的代碼分開以減少V層代碼臃腫問題。
  4. SpriteKit引擎:iOS7與後來的系統版本中原生內置的新框架。該框架主要用來開發2D遊戲。目前已經支持的內容包括精靈,各種特效,集成了物理引擎庫等許多內容。

【遊戲引擎SpriteKit基礎工作】

  1. 首先將我們熟知某個的UIViewController作爲根視圖引出遊戲內容,與常規iOS應用不同的是其視圖類型爲SKView而非UIView。
  2. SKView是UIView的子類,用於執行場景的呈現。
  3. 所謂場景是SKScene類-用於表達畫面、動畫和渲染遊戲內容的構建。其生命週期如下圖所示,表現出如何渲染每一幀的過程。
    SKScene生命週期
  4. SpriteKit框架下的節點大都是SKNode爲基礎類型進行子類擴展而生的。系統自帶的包括SKSpriteNode、SKShapeNode、SKLabelNode、SKVideoNode等。
  5. SpriteKit框架下依然存在許多委託方法,可以在協議中去實現它們來達成一些事件的處理。例如SKSceneDelegateSKPhysicsContactDelegate
  6. 使用SKAction來達成SKNode對象的執行方法,使用SKTranstion來實現SKScene場景之間的轉換。SKTexture實現內容的紋理外觀。

以上不難看出,遊戲內容是合理使用豐富的類和自定義一些子類來構建出具體的場景,再把場景加入到視圖控制器下的視圖進行呈現,不同場景之間的切換也是在同一個視圖控制器下。(SKNode與其他類型——SKScene——SKView——UIViewController)

【遊戲APP的項目結構】

  1. ViewController:包括管理菜單的根視圖器與登錄註冊,排行榜等。
  2. View:包括背景視圖,Toast提示類,Loading類等自定義類。
  3. Nodes:充當Model的類,包括障礙物,世界,道具,人物類以及常量接口文件。
  4. Scenes:遊戲內容的主體場景,包括菜單場景,遊戲場景,結束場景。
  5. 資源文件:Assets.xcassets中存放的圖片資源,Sounds文件夾下的各種音頻資源。

個人認爲,在整個APP中,SKScene一邊對於遊戲部分扮演着呈現畫面的VC角色,一邊扮演着對於整個APP來說的Model角色,因此可以看作雙層的MVC結構。
遊戲內容的MVC關係如下:在這裏插入圖片描述

將整個遊戲場景看作APP的Model,上層的MVC關係如下:在這裏插入圖片描述

【主菜單實現】

LJZMenuScene(SKScene子類)——SKView(UIView子類)——GameViewController(UIViewController子類)

  1. 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
  1. 完成生命週期方法以及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];
}
  1. 菜單場景初始化,場景內容在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
  1. 以上可以看到關於角色、地板是由其他類來創建的,除此外還包括了障礙物與遊戲道具等遊戲組件,這些是抽象出的Model類用來方便在各個場景中管理,在後面的內容我會詳細說明。

本篇主要講述了SpriteKit引擎開發小遊戲的總體結構與主視圖實現,下一篇我將展示主菜單中賬號功能、排行榜、以及遊戲組件的內容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章