(Mac) 002 - How to Create a Game Window using MacOS and AppKit
Table of Contents
I've making the macOS version of the game and the first step is to create a Window.
These steps is not much complicated, except by the Apple macOS documentation, that is very poor of examples.
So, I found the great work of Theodore Bendixson and I follow along.
So, In the next lines I'll show you how to create a window and set up the main game loop.
All these steps require the Apple documentation, I'll just show the code and explain only the key points, assuming you'll refer to the documentation alongside.
It's so important to follow the documentation side by side with this post.
Also, a little bit knowledge of Objective-C is required, because we mix the Objective-C and C++. Objective-C++ at the end of the day.
Build with AppKit
Create a Window
int main(int argc, char **argv) { u32 window_width = 1280; u32 window_height = 720; NSRect window_rect = NSMakeRect((screen_rect.size.width - window_width) * .5, (screen_rect.size.height - window_height) * .5, window_width, window_height); u32 window_flags = NSWindowStyleMaskClosable| NSWindowStyleMaskTitled| NSWindowStyleMaskMiniaturizable; NSWindow *window = [[MacNSWindow alloc] initWithContentRect:window_rect styleMask:window_flags backing:NSBackingStoreBuffered defer:NO]; MacWindowDelegate *mac_window_delegate = [[MacWindowDelegate alloc] init]; [window setBackgroundColor:NSColor.blackColor]; [window setTitle:@"Handmade"]; [window setDelegate:mac_window_delegate]; [window makeKeyAndOrderFront:nil]; }
The Apple/AppKit uses the OOP, and many of the constructors has a easier initializer like showing before;
The key points is:
- MacNSWindow: A subclass of NSWindow that I've created to fix future problems with sound and keyboard; Don't worry about it for now.
- NSMakeRect: This is a helper that make a Rectangle creation much more easier;
- NSWindow: The Window itself represented by NS Apple pattern;
- NSBackingStoreBuffered: Specifies how the drawing done in the window is buffered by the window device. Nowadays, we need render into a display buffer and then flushes it to the screen.
- makeKeyAndOrderFront: Moves the window to the front of the screen list and enable the key window;
In fact, I don't know the best way how to bring window to the front. But, I found two calls that "solved the problem" for while.
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; [NSApp activateIgnoringOtherApps:YES];
However, these methods make the mouse track movement outside the window. I really don't know how to fixed yet.
Window Procedure
Now, let's see how to implement the callback function that can be used to handle the window events like close button, resize the window, etc.
First, I create some delegate (interface) to conforms the AppKit classes.
@interface MacWindowDelegate: NSObject<NSWindowDelegate>; @end @interface MacNSWindow: NSWindow; @end
The next step is to implement that protocol and create a variable that change handle the close button and terminate the main loop.
static bool should_quit; @implementation MacWindowDelegate - (void)windowWillClose:(NSNotification *)notification { should_quit = true; // this control the game loop } @end @implementation MacNSWindow - (void)keyDown:(NSEvent *)event { } @end
I had to override the method keyDown
to avoid a "strange" sound when typing keys (this only occurs later).
Process Messages For Game-Loop
The next step is to process messages from the message queue.
These messages include window messages, keyboard input, mouse input, and others.
We'll use the nextEventMatchingMask
to retrieve event messages from the queue.
Of course, we must perform this entire process inside a main loop controlled by a boolean variable. Otherwise, the window will flash and close instantly.
I declare this bool variable named should_quit
.
Static Keyword And Process Message Queue
The variable is global scope and is declared with the static
keyword.
The meaning of static varies depending on the context in which it appears.
For example, if static
is used on local scope variable. It behaves like a persistent local variable.
When used within a function, it restricts to the file of translation unit. In the others words, it becomes an internal
function.
Let's take a look at the code:
At the top of the file, we have a global variable named should_quit
.
Inside the main function, I had:
while(!should_quit) { NSEvent *event; do { event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES]; switch ([event type]) { default: { [NSApp sendEvent:event]; } } } while(event != nil); } return 0;
The final step is to add a static library AppKit.framework
to the build.sh file.
LINKER_FLAGS="-framework AppKit" COMPILER_FLAG="-g" clang $COMPILER_FLAG -o handmade ../src/mac_handmade.mm $LINKER_FLAGS
Conclusion
To create a window we need to determine a window class that contains title and properties for the window. It also includes the window callback procedure to handle events like close the window.
Now, create a window with coordinates, title and some flags to define window properties like borders, visible, etc.
Create a main-loop to process current thread message queue and window message queue.
This process allow us to translate the virtual-key into character message and dispatch this message to the the window callback procedure.
My Quick Access
- Create Window
- Create Window Delegate (interface/implementation)
- Create GameLoop
- Process Event Queue with nextEventMatchingMask
- Extra: Paint the whole Window with setBackgroundColor from NSWindow