First Impressions
So let me start off by saying, Swift is FAR from finished. I know Apple is calling it “released”, but that’s a load of garbage. My two main annoyances with swift from the previous post reign true, alongside a new victor: the infamous can-go-to-hell sourcekit crash. This problem occurs so frequently on the latest Xcode to GM candidate, that I came very very near abandoning Swift as the language for this game entirely yesterday. If it wasn’t for a post that I found on the Apple developer forums related to deleting the “ModuleCache” dir within XCode’s DerivedData, I would have probably had to abandon swift which is really sad considering how much effort apple has put into it already. Let me just say that a craftsman is only as good as his tools, and when it comes to XCode 6 alongside Swift, at this point I’d be very very cautious using it for anything that you absolutely must count on to be finished in a reasonable timeline. This goes without saying that apple seriously needs to re-evaluate what they consider to be a GM release.
Either way… Because my project is an experiment and has no real pressure of any release deadline (apart from the fictional one I’ve set for myself), I will continue using Swift as the language. After using it to put together the stuff that I’m going to show today, I must say that I definitely like coding in Swift compared to Objective-C for class design. It’s a much less uglier language for the most part. Some things still tick me off such as continuos need for explicit casting. But overall, I can see its appeal as a language choice over Objective-C. I say over ObjC because if you are stuck using Objective-C, you’re already stuck with an OSX/iOS-only application. So you might as well make things easier on yourself and use Swift instead where possible (once it matures).
Lastly, I’m not setting out to use every single feature swift has to offer. I likely won’t touch a lot of them. The whole point here is to get the game done and to use the language as exercise to see what it offers to me to put together A 3D game in the OOP-style that I’m used to using. My background is in C++, so I’ll likely find certain features more useful such as nested type capabilities.
The Game Architecture
So writing about programming is always more complicated than writing about 3D modeling or artwork. At the beginning of this project, I stated that I would care more about the game than the programming itself. That has already proven difficult for me, since my years of experience programming always leads me to architect a good solution for the task at hand. One of the things I can’t stand in a game (or program in general) is over-architecture. So what I present below hopefully won’t change that much moving forward. Which leads me to say that I really want to focus on the game now. Now that I’m in the main stretch of developing this thing, I need to put all efforts on the game development itself. This means I might be ramping down temporarily the number of posts on this blog until the game begins nearing completion. I hope to be at that phase within the next two weeks.
Graphics Engine
As I’ve mentioned in the past, the game will take the form of a series of swift classes that are going to be built on top of my existing Objective-C codebase. This is the same codebase that powers my Verto Studio 3D modeling tool that I’ve used to put assemble the 3D assets. Because Swift is designed to interoperate with ObjC pretty easily, this shouldn’t be a problem. A closely-related C++11 mirror of the lowest layer of this system is available open-sourced at https://github.com/mlfarrell/VGLPP.
I loosely refer to all of this code as a “graphics engine”. It contains routines for working with OpenGL 3.2 to draw 3D graphics primitives (VGL layer), and load/render 3D scenes and models (VOM / Object-Model) layer. In the Objective-C code, these layers blur a bit but you get the idea. The important classes that I interact with in the game (in the swift code) are as follows. Because this system is quite large, this list is not exhaustive.
Objective-C Classes (Graphics Engine)
VGL Layer
- VertoGLUserShaderEffect (subclassable to provide shader effects)
- VertoGLStateMachine (GL 1.1-like state and state delegation)
- VertoGLSystem (delegation and control for the behavior of the entire VGL system)
- VecTypes (C structs for low-level OpenGL-needed things like float3, float4, float16, etc).
- MatTypes (C structs for low-level OpenGL-needed math things like mat3, mat4, etc).
VOM Layer
- SceneManager (3D Scene)
- Entity (3D Model type)
- EntityMaterial (Material properties)
- Texture2D (Texture map)
- RenderPassManager (Multipass rendering manager)
The Game Classes
After one day, here’s the basic architecture I have for the game. The idea here follows the same concept I’ve used for many years now: a game loop class with three methods that manage input (processEvents), processing(processLogic/update) and output (rendering). At any given time, the game loop will be in one of many game states. These states dictate how the game loop behaves. Currently the planned game states are Splash (loading up), Menu (the title menu), Driveby (the actual gameplay), and Game over. The game states are managed and executed from the game loop using a new GameStateRunner protocol type. The game state holds on to a single object conforming to the GameStateRunner protocol and forwards the processEvents, processLogic and render methods to it accordingly. This allows me to nicely separate and organize my code into special GameStateRunner conforming classes. The interesting thing here is that swift actually allows me to embed some logic into the game state enum itself which I’ve already found useful to allow it to load and construct the GameStateRunner objects for me. Confused yet? Awesome!
Objective-C Classes (Driveby Gangster Game)
- GameLoop (base class that interacts with SDL C-API and sets up OpenGL, this stuff is just simpler to do in C (which Objective-C is fully backwards compatible with compared to swift)
- SkeletalAnimationShaderEffect : VertoGLUserShaderEffect
Swift Classes (Driveby Gangster Game)
Subclassing or Protocol conformance denoted with colon notation
- GangsterGameLoop : GameLoop
- DrivebyGameStateRunner : GameStateRunner
- TitleMenuStateRunner : GameStateRunner
- SplashStateRunner : GameStateRunner
- GameOverStateRunner : GameStateRunner
- GameObject (base class game object for all visual objects in the game)
- GameObjectSkeletalAnimated : GameObject (any game object that has a skeletal animation)
- GameObjectPlayer : GameObjectSkeletalAnimated (the game player / protagonist)
- GameObjectEnemy : GameObjectSkeletalAnimated (enemies in the game)
- GameObjectBystander : GameObjectSkeletalAnimated (bystanders in the game)
- GangsterSceneManager : SceneManager (game-oriented scene manager subclass)
Now the good news is, above should be just about it. All of the shadow stuff, shaders, texturing, rendering of the game scene is all saved in the Verto Studio file and should load and render just like I had it in the editor when I load it into the game via SceneManager’s load methods. The skeletal animation support (apart from the special shader effect subclass) is also all entirely provided by my graphics engine so I shouldn’t have to tinker too much with that stuff besides optimization.
Now this post has already gotten crazy long and it would be too much to include the source code of all of the above classes in this post. So instead I’m going to provide snippets of some of the base classes so you can understand how the system will work.
The Game Loop subclass in Swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
//
// Simple3DGameLoop.swift
// Quick 3D Game
//
// Created by Mike Farrell on 8/30/14.
// Copyright (c) 2014 Mike Farrell. All rights reserved.
//
import Foundation
public class GangsterGameLoop: GameLoop
{
enum GameState
{
case Splash
case Menu
case Driveby
case GameOver
func load(#gameLoop: GangsterGameLoop) -> GameStateRunner!
{
switch self
{
case .Driveby:
var state: GameStateRunner! = nil
autoreleasepool
{
state = DrivebyGameStateRunner(gameLoop: gameLoop)
}
//reset shared scene manager to the main scene
if let drivebyState = state as? DrivebyGameStateRunner
{
drivebyState.scene.becomeSharedSceneManager()
}
return state
default:
return nil
}
}
}
private var lastTime: CFAbsoluteTime = 0
private var fps = 0, actualFps = 0
private var fpsTimer: NSTimer? = nil
private var gameStateRunner: GameStateRunner? = nil
private var gameState: GameState = .Splash
private var vsyncOn = true
//MARK: - Construction and setup -
override init()
{
super.init()
}
deinit
{
fpsTimer?.invalidate()
}
func load()
{
lastTime = CFAbsoluteTimeGetCurrent()
fpsTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "fpsTimer:", userInfo: nil, repeats: true)
gameState = .Driveby
gameStateRunner = gameState.load(gameLoop: self)
}
public override func initOpenGL()
{
super.initOpenGL()
glEnable(GLenum(GL_DEPTH_TEST))
glClearColor(0, 0, 0, 1)
load()
}
public override func shutdown()
{
//force-dealloc these things synchronously before the GL context goes down
gameStateRunner = nil
super.shutdown()
}
//MARK: - Game loop
public override func processLogic()
{
let delta = (CFAbsoluteTimeGetCurrent()-lastTime)*60.0
lastTime = CFAbsoluteTimeGetCurrent()
gameStateRunner?.processLogic(delta: Float(delta))
}
public override func keyDown(keysym: SDL_Keycode)
{
switch Int(keysym)
{
case SDLK_v:
toggleVsync()
default:
break
}
}
public override func processEvents()
{
super.processEvents()
gameStateRunner?.processEvents()
}
public override func render()
{
glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))
gameStateRunner?.render()
fps++
}
//MARK: - Misc
func fpsTimer(timer: NSTimer)
{
actualFps = fps
println("FPS: \(actualFps)")
fps = 0
}
func toggleVsync()
{
vsyncOn = !vsyncOn
SDL_GL_SetSwapInterval(vsyncOn ? 1 : 0)
println("Vsync is \(vsyncOn)")
}
} |
Using the ability to embed a “load” method directly in the game state enum is probably the most “swift-esque” thing I’ve done sofar. Moving onto the GameStateRunner protocol and sample implementation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//
// GameState.swift
// Gangster Driveby
//
// Created by Mike Farrell on 10/15/14.
// Copyright (c) 2014 Mike Farrell. All rights reserved.
//
import Foundation
protocol GameStateRunner
{
func processLogic(#delta: Float)
func processEvents()
func render()
} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
//
// DrivebyGameState.swift
// Gangster Driveby
//
// Created by Mike Farrell on 10/15/14.
// Copyright (c) 2014 Mike Farrell. All rights reserved.
//
import Foundation
class DrivebyGameStateRunner : GameStateRunner
{
private unowned let gameLoop: GangsterGameLoop
private var t: Float = 0
var scene: GangsterSceneManager! = nil
var gameObjects: [GameObject] = []
var player: GameObject!
init(gameLoop gl: GangsterGameLoop)
{
gameLoop = gl
NSKeyedUnarchiver.setClass(GangsterSceneManager.self, forClassName: "SceneManager")
let path = NSBundle.mainBundle().pathForResource("street scene", ofType: "vssproj")
scene = GangsterSceneManager(contentsOfFile: path)
scene.activeCamera.distance = 20.0
scene.occlusionCulling = true
scene.skeletalEffect = SkeletalAnimationShaderEffect()
scene.optimize()
//load game objects
loadModels()
println("entities: \(scene.entities.count)")
player.model.pos = float3(x: 20, y: 3, z: 10)
//shadow map resolution
//scene.sceneShadowMappingPass()?.setDimensionsWidth(8192, andHeight: 8192, andTextureFormat: GLenum(GL_FLOAT))
}
private func loadModels()
{
let scale: Float = 0.1
//load models
let gangsterPath = NSBundle.mainBundle().pathForResource("gangster", ofType: "vssproj")
let gangsterScene = SceneManager(contentsOfFile: gangsterPath)
var result = gameLoop.loadAnimatedModel("walk.dae", intoScene: scene)
var modelInfo = AnimatedModelInfo(animatedModelEntity: result["entity"] as Entity, aiScene: result["aiScene"] as AssimpSceneWrapper,
globalInverseTransform: gameLoop.globalInverseTransform())
var obj: GameObjectSkeletalAnimated = GameObjectEnemy(model: gangsterScene.entities.firstObject as Entity, animatedModelInfo: modelInfo)
obj.model.scale = float3(x: scale, y: scale, z: scale)
gameObjects.append(obj)
obj.addToScene(scene)
//for now
player = obj
let bystanderPath = NSBundle.mainBundle().pathForResource("bystander", ofType: "vssproj")
let bystanderScene = SceneManager(contentsOfFile: bystanderPath)
obj = GameObjectBystander(model: bystanderScene.entities.firstObject as Entity, animatedModelInfo: modelInfo)
obj.model.pos = float3(x: -20, y: 3, z: 20)
obj.model.scale = float3(x: scale, y: scale, z: scale)
gameObjects.append(obj)
obj.addToScene(scene)
}
private func isKeyDown(code: SDL_Scancode, _ key: UnsafePointer<Uint8>) -> Bool
{
return key[Int(code.value)] != 0
}
func processEvents()
{
let key = gameLoop.keyState
let activeCamera: CameraView = scene.activeCamera
if isKeyDown(SDL_SCANCODE_EQUALS, key)
{
activeCamera.distance -= 1
}
if isKeyDown(SDL_SCANCODE_MINUS, key)
{
activeCamera.distance += 1
}
if isKeyDown(SDL_SCANCODE_LEFT, key)
{
player.model.translate(float3(x: 0, y: 0, z: 1))
}
if isKeyDown(SDL_SCANCODE_RIGHT, key)
{
player.model.translate(float3(x: 0, y: 0, z: -1))
}
if isKeyDown(SDL_SCANCODE_UP, key)
{
player.model.translate(float3(x: -1, y: 0, z: 0))
}
if isKeyDown(SDL_SCANCODE_DOWN, key)
{
player.model.translate(float3(x: 1, y: 0, z: 0))
}
}
func processLogic(#delta: Float)
{
t += 0.1*delta
let activeCamera: CameraView = scene.activeCamera
activeCamera.viewMode = CAM_FREE
activeCamera.theta = sinf(t*0.05)*30
var bbox = float3.zero
player.model.getBBox(&bbox)
var pos = player.model.pos
pos.y += 0.1*bbox.y/2
activeCamera.setSphericalPos(pos)
for obj in gameObjects
{
obj.update(delta: delta)
}
}
func render()
{
scene.render()
}
} |
Lastly, a sample lineage of the early GameObject class implementation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
//
// GameObject.swift
// Gangster Driveby
//
// Created by Mike Farrell on 10/14/14.
// Copyright (c) 2014 Mike Farrell. All rights reserved.
//
import Foundation
class GameObject
{
///Graphical 3D model used to render the game object
let model: Entity
///Per-object time in seconds since spawn
var time: Float = 0
///Scene that the model is currently rendered in
weak var scene: GangsterSceneManager? = nil
init(model m: Entity)
{
model = m
}
deinit
{
if(scene != nil)
{
removeFromScene(scene!)
}
}
///Process logic updates for the game object
func update(#delta: Float)
{
time += delta/60.0
}
func addToScene(scene: GangsterSceneManager)
{
self.scene = scene
scene.entities.addObject(model)
}
func removeFromScene(scene: GangsterSceneManager)
{
scene.entities.removeObject(model)
}
} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
//
// GameObjectBystander.swift
// Gangster Driveby
//
// Created by Mike Farrell on 10/15/14.
// Copyright (c) 2014 Mike Farrell. All rights reserved.
//
import Foundation
class GameObjectBystander : GameObjectSkeletalAnimated
{
override init(modelReference m: Entity, aiScene ais: AssimpSceneWrapper)
{
super.init(modelReference: m, aiScene: ais)
setupMaterial()
}
override init(model m: Entity, animatedModelInfo: AnimatedModelInfo)
{
super.init(model: m, animatedModelInfo: animatedModelInfo)
setupMaterial()
}
func setupMaterial()
{
//use random materials for the suit for bystanders
let suitMaterial = model.submeshes[1].mat
suitMaterial?.diff = ColorMaker.float4FromHue(CGFloat(drand48()), saturation: 0.2, brightness: 0.8)
let shinyColor = Float(drand48())
suitMaterial?.spec = makeFloat4(shinyColor, shinyColor, shinyColor, 1)
}
override func update(#delta: Float)
{
super.update(delta: delta)
}
} |
That was alot. The end.