Finally done the porting of where I was when I left off on the swift side!
Commiting this code and ready to resume actual game development!
One of the reasons why love game programming so much is that it forces you to do things that would otherwise seem quite ridiculous. I have always liked to shoot first and ask questions later when it comes to implementing an idea. Sometimes it’ll just pop in my head and then I will start working on it and before I know it…it’s done. The last week and a half has been more work than I ever wanted to do but the result is awesome. A very large portion of my entire Verto Studio graphics engine and code base has now been rewritten in C++ and is now portable to any operating system.
This means I can now load and render my Verto Studio files on Windows, Linux, anything I want without any strict dependencies on Mac anymore. As a game developer, this is an awesome feeling and I am beyond stoked. I worked very hard on the Verto Studio tech over the last three years and to finally free it from OSX/iOS is great.
I’ve moved on the actual swift code of the in-progress Driveby Gangster Game project and hopefully I’ll have that done soon next week. All-in-all I’d say the extra week of work was definitely worth it.
Too much talk about code lately on here.
Here’s some latest screenshots. Before my decision to switch languages, I completely finished off the “practice mode”
I also recorded a gif during my testing with the bullet collision detection against arbitrary polygons in the scene.
So it had to happen…
The more I work on this game the more I realize that I kind of like it… and that means there’s certain things need to change. For starters, it’s definitely running longer than I expected it to (the original two-week estimate now sounds pretty “out there”). For this reason, I need to shave down on the programming time. It has become pretty obvious to me that for every 1/2 hour that I spend writing code in swift for this game, 10 minutes of it is spent screwing around with the language and that is friction then I can no longer accept. So I’m dropping swift for this game.
Since I’m switching languages and I’m going to be rewriting some code, I decided to open things up quite a bit and go back to C++. It’s going to cost me roughly a week or two to rewrite things, but the benefit is going to be huge. For starters, since this game depended on my very large and very complicated Objective-C graphics engine (Verto Studio), I now have the “opportunity” to continue rewriting all of this code in C++ (this was originally started on my github as VGLPP). This means that this game and any other game that I want to use with the ported engine will be able to run on ANY platform including windows. This is a major win for me since making a game for OSX/iOS only isn’t really that smart. I’ve been wanting to do this for awhile and my frustrations with swift finally pushed me to do it.
So that’s what I’m going to be up to for awhile. As I’ve mentioned on twitter, I’ll be streaming roughly twice a week on my twitch channel for anyone who is curiuos about the process and wants to follow along. http://twitch.tv/vertostudio3d
I’ve already gotten quite a bit done and have surpassed my biggest barrier for doing this which was finding a reasonable way to parse verto studio files (spat out by NSKeyedArchiver/NSCoder objective-c mechanisms) in a portable way in C++. TinyXML and a little of reverse engineering did the trick.
With respect to all of the “benefits” swift and Objective C were providing over a lower level language such as C++, I’ve essentially found a reasonable replacement for each one thanks to C++11′s new features.
Swift/Objc | C++11 |
Strong Pointer (Reference Counting) | std::shared_ptr |
Weak Pointer (Reference Counting) | std::weak_ptr |
Blocks & Closures | Lambda Functions |
Protocols | Multiple Inheritence |
Dynamic Typechecking via “isKindOfClass” | C++ typeid / RTTI |
So you get the idea, nothings really out of reach with C++. It’s still more annoying and I’m not 100% thrilled with dealing with things like templates, header files and verbose syntax again, but I’ll take it over an unstable language any day.
I’ve been less active on here because I’ve been writing quite a bit of code. Like I said previously, I’m in the main swing of developing this game. This is where things start to get crazy. Namely, I’ve added quite a bit of classes to the project to handle everything from basic collisions and 3D math extensions to generating and displaying 3D text on the screen. I feel like (despite my previous post regarding Swift) things are keeping organized quite well and I haven’t strayed too far from my original architecture plan. I’ve spent most of my time working on the PracticeGameStateRunner class which runs the “Target Practice” initial level in the game. This mini level serves as a point for the player to learn the very simple controls and game mechanics of the game… and it’s serving excellently as a sandbox for me to test these all during development as well.
I’ll cover just a few pieces of code today to show the changes that I’ve made regarding the game State Runner protocol, and some cool stuff that I’ve been able to do with “smart” enums in swift. There’s a lot more I can talk about that I don’t want this post to go on forever.
State runner protocol. Now I have some stuff in there to quickly respond to “game controller” and mouse events, all stemming from the game loop class.
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 |
//
// 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()
func keyDown(keysym: SDL_Keycode)
func keyUp(keysym: SDL_Keycode)
func mouseMoved(event: SDL_MouseMotionEvent)
func mouseDown(event: SDL_MouseButtonEvent)
func mouseUp(event: SDL_MouseButtonEvent)
func joystickDown(controllerButton button: GangsterGameLoop.ControllerButton)
func joystickUp(controllerButton button: GangsterGameLoop.ControllerButton)
} |
Mesh Line Collider – a badly needed construct to determine whether or not a bullet-trajectory would intersect a polygonal mesh in the scene or not (and if so where). I tried porting this over to ObjC before swift and it was a nitemare. Swift’s version is definitely simpler thanks to operator overloading and multiple return values. Note the unsafe pointer craziness in swift which is considerably easier to deal with in C. (ported from http://geomalgorithms.com/a06-_intersect-2.html)
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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
//
// MeshLineCollider.swift
// Gangster Driveby
//
// Created by Mike Farrell on 10/30/14.
// Copyright (c) 2014 Mike Farrell. All rights reserved.
//
import Foundation
class MeshLineCollider
{
weak var entity: Entity!
var boundingBoxCollider: MeshLineCollider?
let verts: GrowableArray
let faceIndices: GrowableArray
init(meshEntity: Entity)
{
entity = meshEntity
verts = entity.editVerts.mutableCopy() as GrowableArray
entity.applyTransformToVerts(verts, andNorms: nil)
let s: SubMesh = entity.submeshes[0] as SubMesh
faceIndices = s.editInds.mutableCopy() as GrowableArray
generateBoundingBoxCollider()
}
init(verts: GrowableArray, faces: GrowableArray)
{
self.verts = verts
faceIndices = faces
boundingBoxCollider = nil
}
convenience init(var verts: [float3], var faces: [uint3])
{
let vBuffer = UnsafeMutableBufferPointer(start: &verts, count: verts.count)
let vgArray = GrowableArray(data: vBuffer.baseAddress, ofSize: Int32(verts.count), andElementSize:Int32(sizeof(float3)), andExtraCapacity: 0)
let fBuffer = UnsafeMutableBufferPointer<uint3>(start: &faces, count: faces.count)
let fArray = GrowableArray(data: fBuffer.baseAddress, ofSize: Int32(faces.count*3), andElementSize:Int32(sizeof(uint)), andExtraCapacity: 0)
self.init(verts: vgArray, faces: fArray)
}
private final func split(quad: uint4) -> (uint3, uint3)
{
return (uint3(x: quad.x, y: quad.y, z: quad.w), uint3(x: quad.w, y: quad.y, z: quad.z))
}
private func generateBoundingBoxCollider()
{
var bbSize = float3.zero
entity.getBBox(&bbSize)
let box = Box3D(dimensions: bbSize, andPosition: float3.zero)
let boxVerts = box.generateVertices()
entity.applyTransformToVerts(boxVerts, andNorms: nil)
var boxInds: [uint4] = [ ]
boxInds.append(uint4(x: 0, y: 1, z: 2, w: 3))
boxInds.append(uint4(x: 0, y: 4, z: 5, w: 1))
boxInds.append(uint4(x: 2, y: 6, z: 7, w: 3))
boxInds.append(uint4(x: 3, y: 7, z: 4, w: 0))
boxInds.append(uint4(x: 1, y: 5, z: 6, w: 2))
boxInds.append(uint4(x: 4, y: 5, z: 6, w: 7))
var triangles: [uint3] = [ ]
for quad in boxInds
{
let spl = split(quad)
triangles.append(spl.0)
triangles.append(spl.1)
}
let fBuffer = UnsafeMutableBufferPointer<uint3>(start: &triangles, count: triangles.count)
let fArray = GrowableArray(data: fBuffer.baseAddress, ofSize: Int32(triangles.count*3), andElementSize:Int32(sizeof(uint)), andExtraCapacity: 0)
boundingBoxCollider = MeshLineCollider(verts: boxVerts, faces: fArray)
}
func debugRender()
{
if boundingBoxCollider != nil
{
boundingBoxCollider?.debugRender()
return
}
glDisable(GL_CULL_FACE.e)
glDepthRange(0.0, 0.9999);
let vgl = VertoGLStateMachine.currentMachine()
vgl.vertexPointerOfSize(3, type: GL_FLOAT.e, stride: 0, pointer: verts.data, bufferLength: GLsizeiptr(Int32(sizeof(float3))*verts.count))
vgl.setPrimaryColor(makeFloat4(0.2, 0.2, 0.2, 1))
vgl.prepareToDraw()
vgl.drawElementsInMode(GL_TRIANGLES.e, andCount: GLsizei(faceIndices.count), type: GL_UNSIGNED_INT.e, indexPointer: faceIndices.data,
bufferLength: GLsizeiptr(Int32(sizeof(uint))*faceIndices.count))
glDepthRange(0.0, 1.0);
}
//MARK: - Collision with line
func rayIntersection(rayOrigin ro: float3, rayDirection rd: float3, inout intersectionPoint: float3) -> IntersectionPoint?
{
if let bbox = boundingBoxCollider
{
if bbox.rayIntersection(rayOrigin: ro, rayDirection: rd, intersectionPoint: &intersectionPoint) == nil
{
return nil
}
}
let pointSet = IntersectionPointSet()
for i in 0..<faceIndices.count/3
{
let result = triangleIntersection(triangleIndex: Int(i), rayOrigin: ro, rayDirection: rd, intersectionPoint: &intersectionPoint)
if result.0
{
pointSet.add(IntersectionPoint(distance: result.1, point: intersectionPoint))
}
}
if !pointSet.empty()
{
return pointSet.nearest()
}
return nil
}
private func triangleIntersection(triangleIndex i: Int, rayOrigin ro: float3, rayDirection rd: float3, inout intersectionPoint ip: float3) -> (Bool, Float)
{
let smallNum: Float = 0.00000001
var u: float3, v: float3, n: float3 // triangle vectors
var dir: float3, w0: float3, w: float3 // ray vectors
var r: Float, a: Float, b: Float // params to calc ray-plane intersect
let vData = UnsafeMutableBufferPointer<float3>(start: UnsafeMutablePointer<float3>(verts.data), count: Int(verts.count))
let iData = UnsafeMutableBufferPointer<uint3>(start: UnsafeMutablePointer<uint3>(faceIndices.data), count: Int(faceIndices.count/3))
let v0 = vData[Int(iData[i].x)], v1 = vData[Int(iData[i].y)], v2 = vData[Int(iData[i].z)]
// get triangle edge vectors and plane normal
u = v1 - v0
v = v2 - v0
n = u.cross(v) // cross product
if n == float3.zero // triangle is degenerate
{
// do not deal with this case
return (false, 0)
}
dir = rd
w0 = ro - v0
a = -n.dot(w0)
b = n.dot(dir)
if fabsf(b) < smallNum
{
return (false, 0)
}
// get intersect point of ray with triangle plane
r = a / b
if r < 0.0 // ray goes away from triangle
{
return (false, 0) // => no intersect
}
// for a segment, also test if (r > 1.0) => no intersect
// intersect point of ray and plane
ip = ro + dir*r
// is I inside T?
var uu: Float, uv: Float, vv: Float, wu: Float, wv: Float, D: Float
uu = u.dot(u)
uv = u.dot(v)
vv = v.dot(v)
w = ip - v0
wu = w.dot(u)
wv = w.dot(v)
D = uv * uv - uu * vv
// get and test parametric coords
var s: Float, t: Float
s = (uv * wv - vv * wu) / D
if s < 0.0 || s > 1.0 // I is outside T
{
return (false, 0)
}
t = (uv * wu - uu * wv) / D
if t < 0.0 || (s + t) > 1.0 // I is outside T
{
return (false, 0)
}
return (true, r) // I is in T
}
} |
Cool little WIP controller button enum nested in the game loop class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
enum ControllerButton: Int
{
case Shoot = 9
case Aim = 8
var name: String
{
let buttonNames: [ControllerButton: String] = [ .Shoot: "Shoot", .Aim: "Aim" ]
return buttonNames[self]!
}
var obj: NSNumber
{
return NSNumber(integer: self.rawValue)
}
} |
Okay so when you use a language daily, you begin to really pick up some opinions and feelings about it that tend to really stick with you. With swift, I definitely have some of those.
Let me focus on just two… of the things that really irk me about the freaking swift language.
This is my number one gripe with the swift language. The compiler just wont shut the hell up and do what I was intending. Now I know that apple has designed the language to absolutely not allow any implicit casting, and I understand their reasons behind this, but man, you’ve taken one benefit, and added 10-fold the costs. Requiring explicit casting takes code in swift that should look extremely simple and adds about 3-4 explicit constructor-casts to it causing the code to display as bloated, confusing ill-intended crap. What really bothers me is that this is the case even for upgrading types such as assigning a float into a double. Another problem is that in the C world (all of the APIs that we work with on a daily basis) there might be 4 or 5 different integer types (int, Sint32, GLuint, etc) and multiple float types (GLfloat, float, CGFloat, etc). Casting between these in C/C++/Objc is completely harmless 99% of the time and therefore done implicitly. In swift, I have to pepper this code with constructor casts every, single, freaking time.
In C++, you could allow implicit types by overloading the constructor so that an expression such as int x = someFloat would be the same as a constructor for int that takes a float as an argument. Swift badly needs this.
For now I’ve gotten to the point where I’ve added very simple computed properties to the various types in swift to at least make some of this casting more painless. Just a few of those extensions are below.
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 |
//
// CastingHelpers.swift
// Gangster Driveby
//
// Created by Mike Farrell on 10/27/14.
// Copyright (c) 2014 Mike Farrell. All rights reserved.
//
import Foundation
//swift is driving me crazy
extension Int
{
var f: Float
{
return Float(self)
}
}
extension Int32
{
var f: Float
{
return Float(self)
}
var e: GLenum
{
return GLenum(self)
}
/*func c<T: IntegerLiteralConvertible>() -> T
{
return T(integerLiteral: i)
}*/
}
extension Sint16
{
var f: Float
{
return Float(self)
}
}
extension UInt
{
var f: Float
{
return Float(self)
}
}
extension Double
{
var f: Float
{
return Float(self)
}
}
extension Float
{
var cg: CGFloat
{
return CGFloat(self)
}
} |
This makes it so that code that would have to look like
1 2 3 4 |
glDisable(GLEnum(GL_DEPTH_TEST))
let f: Float = 3.0
let f2 = f*Float(sin(0.5)) |
Can be…
1 2 3 4 |
glDisable(GL_DEPTH_TEST.e)
let f = (3.0).f
let f2 = f*sin(0.5).f |
Google swift and “sourcekit” and you’ll see, this is nowhere NEAR an actual finished language. Apple has rushed this out the door and I’m honestly sorry that I’ve decided to use it to make a game. Imagine typing code and then every half hour you get 30 seconds of a frozen spinner before ”sourcekit crashed” appears in your IDE. If you’re lucky, it’ll stop for another 5 minutes, but if you aren’t it’ll happen in a loop and you have to go delete your “ModuleCache” dir and quit and restart xcode and start working again…. until about an hour later. This is NOT how I prefer to work. I like the “idea” of swift, but its just way too unstable to use. I’m going to finish this game with it, and then probably stay away from swift for any real projects for another year or so until apple fixes this extremely annoying stability problem.
These two things may seem small, but they’re enough to waste 10 minutes of time for every hour I work on this game, and for me, that’s just completely unacceptable.
Recently worked alot of basic walking pattern AI for the bystander game objects, a practice range which will precede the first “day” (level) in the game, and some cool shader effects using particles for the gangster car kicking up dust while driving through the desert.
Too tired to describe more.
Pictures! Please forgive the quality of the video(s).
Yesterday was a lot of work. I worked really hard to obtain a higher frame rate for the game, because I know that my postprocessing effects will be costly. Sometime certain obvious things take a few days to sink in. It’s funny how I was looking everywhere to try to improve my fill-rate and bandwidth performance when I knew the problem was a polyon issue. Drawing the entire scene twice (once for lightmap/shadow pass and again for actual rendering) was really stupid. I was doing this because certain elements in the shadow maps such as the walking men were dynamic, thus the scene needed to be redrawn into the shadowmap every time. However, the mass majority of the polygons in the scene where static. The solution of this was to pre-render this static geometry into one shadow map, and render the dynamic geometry into a separate one, and then adapt all of my shaders to take an optional second shadow texture. This wasn’t easy since much of my architecture wasn’t designed for multiple shadowmaps per light. But I got it working.
The above boosted my framerates back into a very acceptable range so I’m happy. It’s a little annoying, that for some reason on OSX Yosemite, I can no longer turn off Vsync in a windowed mode. So I’ve been having to go fullscreen during debugging to see my max framerates which are hovering now well-over 100.
As far as swift goes, I’m now on the fully released Xcode 6.1 and I’m having far less problems with the source kit crashing which is great. The IDE is generally still a lot slower than its predecessor which I hope improves with time. as I’m writing more and more code in Swift, certain features are starting to really prove useful to me as I get used to the language. Among these are the tuples, willSet/didSet, and operator overloading. On the ObjC side, I’ve had to rely on this bulky MathVector class to do my vector maths. With swift I’ve been able to add all of the math operators I need directly to my low-level C structs via an extension. This has already saved me alot of work and made the math code that I’ve written for the game alot cleaner.
Here’s an example of the vector math extension code 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 143 144 145 146 147 148 149 150 151 152 |
extension float3: Printable
{
static var zero: float3
{
return float3(x: 0, y: 0, z: 0)
}
init(_ x: Float, _ y: Float, _ z: Float)
{
self.x = x;
self.y = y;
self.z = z;
}
subscript(index: Int) -> Float
{
get
{
assert(index < 3 && index >= 0)
switch(index)
{
case 0:
return x
case 1:
return y
case 2:
return z
default:
break
}
return 0
}
set
{
assert(index < 3 && index >= 0)
switch(index)
{
case 0:
x = newValue
case 1:
y = newValue
case 2:
z = newValue
default:
break
}
}
}
var length: Float
{
return sqrtf(dot(self))
}
var lengthSquared: Float
{
return dot(self)
}
var xz: float2
{
return float2(x, z)
}
func normalized() -> float3
{
let len = self.length
return float3(x/len, y/len, z/len)
}
func dot(rhs: float3) -> Float
{
return x*rhs.x + y*rhs.y + z*rhs.z
}
func cross(rhs: float3) -> float3
{
var r = float3.zero
r.x = (y * rhs.z) - (z * rhs.y);
r.y = (z * rhs.x) - (x * rhs.z);
r.z = (x * rhs.y) - (y * rhs.x);
return r
}
public var description: String
{
return "(\(x), \(y), \(z))"
}
}
func + (lhs: float3, rhs: float3) -> float3
{
return float3(lhs[0]+rhs[0], lhs[1]+rhs[1], lhs[2]+rhs[2])
}
func += (inout lhs: float3, rhs: float3)
{
lhs = lhs + rhs
}
func - (lhs: float3, rhs: float3) -> float3
{
return float3(lhs[0]-rhs[0], lhs[1]-rhs[1], lhs[2]-rhs[2])
}
func -= (inout lhs: float3, rhs: float3)
{
lhs = lhs - rhs
}
func / (lhs: float3, rhs: float3) -> float3
{
return float3(lhs[0]/rhs[0], lhs[1]/rhs[1], lhs[2]/rhs[2])
}
func /= (inout lhs: float3, rhs: float3)
{
lhs = lhs / rhs
}
func * (lhs: float3, rhs: float3) -> float3
{
return float3(lhs[0]*rhs[0], lhs[1]*rhs[1], lhs[2]*rhs[2])
}
func *= (inout lhs: float3, rhs: float3)
{
lhs = lhs * rhs
}
func * (lhs: float3, scalar: Float) -> float3
{
return float3(lhs[0]*scalar, lhs[1]*scalar, lhs[2]*scalar)
}
func *= (inout lhs: float3, rhs: Float)
{
lhs = lhs * rhs
}
func == (lhs: float3, rhs: float3) -> Bool
{
return (lhs[0]==rhs[0] && lhs[1]==rhs[1] && lhs[2]==rhs[2])
}
func != (lhs: float3, rhs: float3) -> Bool
{
return !(lhs == rhs)
} |
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.
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.
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.
VGL Layer
VOM Layer
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!
Subclassing or Protocol conformance denoted with colon notation
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.