Game Object, Objects Caches and Renderer (Metal Part 5)
Organizing the code in sub classes to build the foundations of what is to come for this basic game engine. Delegating the rendering to an MTKViewDelegate, and also drawing the primitives in the dedicated Game Object class.
Source Code
References
Table of Content
Objects Caches
We will extract the code that initializes the MTLLibrary, MTLRenderPipelineState, MTLRenderPipelineDescriptor and MTLVertexDescriptor into separate classes.
Each of these classes will act as a cache, with a dictionary mapping enum values to objects instances.
The pseudocode structure of these library classes will be like:
1enum ElementType{ 2 case Basic 3} 4 5class ElementsCache<T> { 6 7 private static var _elements: [ElementType: T] = [:] 8 9 public static func Initialize(){ 10 createElements() 11 } 12 13 public static func createElements(){ 14 _elements.updateValue(BasicElement(), forKey: .Basic) 15 } 16 17 public static func GetElement(_ elementType: ElementType)->T{ 18 return _elements[elementType]! 19 } 20 21}
This will allow us to initialize the libraries in the Engine class:
1class Engine { 2 3 public static var Device: MTLDevice! 4 public static var CommandQueue: MTLCommandQueue! 5 6 public static func Initialize(device: MTLDevice){ 7 8 // device is an abstract representation of the GPU 9 // allows to create Metal GPU objects and send them down to the GPU 10 self.Device = device 11 12 // create the command queue to handle commands for the GPU 13 self.CommandQueue = device.makeCommandQueue() 14 15 ShaderCache.Initialize() 16 VertexDescriptorCache.Initialize() 17 RenderPipelineDescriptorCache.Initialize() 18 RenderPipelineStateCache.Initialize() 19 } 20}
Game Object
The game object will be in charge of creating the vertexBuffer and drawing its primitives, for now. In the future this should be handled by the Mesh Renderer component.
The game object also has the vertices array for now, in the future this should be handled by a Mesh class.
1class GameObject { 2 3 var vertices: [Vertex]! 4 var vertexBuffer: MTLBuffer! 5 6 init() { 7 createVertices() 8 createBuffers() 9 } 10 11 func createVertices() { 12 // counter clock wise to define the face 13 vertices = [ 14 Vertex(position: float3(0, 1, 0), color: float4(0, 0, 1, 1)), // top mid 15 Vertex(position: float3(-1, -1, 0), color: float4(0, 1, 0, 1)), // bot left 16 Vertex(position: float3(1, -1, 0), color: float4(1, 0, 0, 1)), // top right 17 ] 18 } 19 20 func createBuffers() { 21 vertexBuffer = Engine.Device.makeBuffer(bytes: vertices, length: Vertex.stride * vertices.count, options: []) 22 } 23 24 func render(renderCommandEncoder: MTLRenderCommandEncoder){ 25 26 renderCommandEncoder.setRenderPipelineState(RenderPipelineStateLibrary.PipelineState(.Basic)) 27 renderCommandEncoder.setVertexBuffer(self.vertexBuffer, offset: 0, index: 0) 28 renderCommandEncoder.drawPrimitives(type: MTLPrimitiveType.triangle, vertexStart: 0, vertexCount: self.vertices.count) 29 } 30}
Renderer
To further simplify the GameView class, we will use a MTKViewDelegate to take on the work of actually rendering the view at 60 fps.
For now it is also in charge of initializing the game objects, but that will be handled by the Scene in the future.
1class GameViewRenderer: NSObject { 2 var gameObject: GameObject = GameObject() 3} 4 5// we will delegate the rendering to this class 6extension GameViewRenderer: MTKViewDelegate { 7 8 func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 9 // when the window is resized 10 } 11 12 func draw(in view: MTKView){ 13 guard let drawable = view.currentDrawable, let renderPassDescriptor = view.currentRenderPassDescriptor else { return } 14 15 let commandBuffer = Engine.CommandQueue.makeCommandBuffer() 16 17 let renderCommandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor) 18 19 self.gameObject.render(renderCommandEncoder: renderCommandEncoder!) 20 21 renderCommandEncoder?.endEncoding() 22 commandBuffer?.present(drawable) 23 commandBuffer?.commit() 24 } 25}
Game View
Now the Game View is just in charge of initializing itself, initializing the Engine, and delegating the rendering to its delegate.
In the future this view will also be in charge of capturing mouse and keyboard inputs.
1class GameView: MTKView { 2 3 var renderer: GameViewRenderer! 4 5 required init(coder: NSCoder) { 6 super.init(coder: coder) 7 8 self.device = MTLCreateSystemDefaultDevice() 9 self.clearColor = Preferences.ClearColor 10 self.colorPixelFormat = Preferences.PixelFormat 11 12 Engine.Initialize(device: self.device!) 13 14 self.renderer = GameViewRenderer() 15 self.delegate = self.renderer 16 } 17}