上一篇我们实现了在macOS上面的编译。但是我们采用的并不是macOS的原生接口,因此获得的OpenGL Context所支持的API版本也被限制到2.1版本。这显然是不够的。
因此,接下来我们来尝试使用macOS的原生图形接口 —— Cocoa,来完成OpenGL Context的创建工作。
Cocoa是基于Objective-C的一套Apple的独有系统。Objective-C是一种比较古老的语言,是C语言的超集,但是与C++语言又非常不同。作者在写这篇文章之前并没有接触过Objective-C语言,所以这两天查阅了大量的资料来“临时抱佛脚”。这里需要特别感谢 @袁全伟 分享的相关资料整理。我把这些资料会同我自己查阅到的资料都列在了参考引用当中。
实际写下来,感觉Cocoa类似Windows平台上的MFC,或者.NET的感觉,封装还是比较彻底的。好处当然是非常简单易用,但是同时也就意味着很多的细节被隐藏。在我早年学习MFC的时候,想要实现同时期Office所提供的一些很酷的控件效果,就发现非常的不容易。后来学习了Win32 API,发现就能很方便的实现了。这两天对Cocoa的突击学习又让我感觉似乎一下子回到了20多年前,找到了那种有力无处使的感觉。
但是到目前为止我还没有能够找到在macOS上比Cocoa更为底层的API接口。所以我们就将就着用吧。
Cocoa里面至少包含了两个Kit:AppKit和UIKit。AppKit提供了应用程序的框架结构,而UIKit提供了UI组件。最为方便地了解Cocoa的方法是使用XCode生成一个基于Cocoa的应用程序项目,这个过程与使用Visual Studio的向导创建项目非常类似,仅仅需要点击几次鼠标,输入应用程序名什么的,就能够生成一个基本的应用程序(含窗体)的基本结构。



这样一个基本的Cocoa应用(含窗体)项目就生成好了。目录结构大致如下:
.
├── Hello Cocoa
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Assets.xcassets
│ │ └── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Base.lproj
│ │ └── MainMenu.xib
│ ├── Hello_Cocoa.entitlements
│ ├── Info.plist
│ └── main.m
└── Hello Cocoa.xcodeproj
├── project.pbxproj
└── project.xcworkspace
└── contents.xcworkspacedata
程序的主入口是 main.m,AppDelegate.h和AppDelegate.m定义了应用程序代理类,也就是可以对标准的Cocoa Application进行扩展的地方。Assets.xcassets当中是应用程序的一些资源文件,比如图标什么的。Base.lproj当中是被称为InterfaceBuilder工具的界面定义文件,就是所谓的WYSIWYG(What You See Is What You Get,所见即所得)的图形界面编辑器文件。Hello_Cocoa.entitlements和Info.plist,这两个都是XML格式的类似manifest的文件,用来向操作系统或者是APN等提供程序相关的meta data的。而Hello Cocoa.xcodeproj目录当中的则是Xcode的项目文件。
我们可以继续在Xcode当中编译这个项目并执行。如果采用命令行,那么编译的命令如下(假设我们在项目根目录):
chenwenlideMBP:Hello Cocoa chenwenli$ xcodebuild -project Hello Cocoa.xcodeproj/ build
然后执行
chenwenlideMBP:Hello Cocoa chenwenli$ build/Release/Hello Cocoa.app/Contents/MacOS/Hello Cocoa
就可以看到我们的第一个Cocoa窗口了:

然而,我们需要如何将它和我们之前的代码结合到一起呢?有没有可能将Objective-C和C++混合进行编程呢?
答案是可以的。Objective-C在经过编译之后,生成的二进制文件是与C语言有着同样的二进制接口的。也就是有二进制兼容性(Swift也是一样)。只不过,Object-C的面向对象模型与C++不同,不能简单地将两者等同起来(也就是运行时间库是不一样的)。在macOS全面采用clang作为编译工具之后,由于llvm中间层的存在,Objective-C与C++的互换变得更为方便,甚至出现了Objective-C++这种可以将两者同时写在一个文件当中的“编程语言”(新版本gcc也支持)。不过这里要注意,实际上Objective-C++并不是一种真正的新的语言,书写在源代码当中的Objective-C代码和C++代码其实是相对独立的被编译处理之后又链接在一起的。
好了,那么接下来让我们基于Xcode所生成的模版,按照我们自己所写引擎的架构和逻辑,编写Cocoa版本的Application和OpenGL Context创建代码。(将Objective-C代码嵌入到我们的C++代码当中)
首先我们将XcbApplication.{hpp,cpp}分别拷贝并改名为CocoaApplication.{h,mm}。mm是Objective-C++源代码的后缀。然后编辑如下:
CocoaApplication.h
#include "BaseApplication.hpp"
#include <Cocoa/Cocoa.h>
namespace My {
class CocoaApplication : public BaseApplication
{
public:
CocoaApplication(GfxConfiguration& config)
: BaseApplication(config) {};
virtual int Initialize();
virtual void Finalize();
// One cycle of the main loop
virtual void Tick();
protected:
NSWindow* m_pWindow;
};
}
#include <string.h>
#include "CocoaApplication.hpp"
#include "MemoryManager.hpp"
#include "GraphicsManager.hpp"
#include "SceneManager.hpp"
#include "AssetLoader.hpp"
#import <AppDelegate.h>
#import <WindowDelegate.h>
using namespace My;
namespace My {
GfxConfiguration config(8, 8, 8, 8, 24, 8, 0, 960, 540, "Game Engine From Scratch (MacOS Cocoa)");
IApplication* g_pApp = static_cast<IApplication*>(new CocoaApplication(config));
GraphicsManager* g_pGraphicsManager = static_cast<GraphicsManager*>(new GraphicsManager);
MemoryManager* g_pMemoryManager = static_cast<MemoryManager*>(new MemoryManager);
AssetLoader* g_pAssetLoader = static_cast<AssetLoader*>(new AssetLoader);
SceneManager* g_pSceneManager = static_cast<SceneManager*>(new SceneManager);
}
int CocoaApplication::Initialize()
{
int result = 0;
[NSApplication sharedApplication];
// Menu
NSString* appName = [NSString stringWithFormat:@"%s", m_Config.appName];
id menubar = [[NSMenu alloc] initWithTitle:appName];
id appMenuItem = [NSMenuItem new];
[menubar addItem: appMenuItem];
[NSApp setMainMenu:menubar];
id appMenu = [NSMenu new];
id quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit"
action:@selector(terminate:)
keyEquivalent:@"q"];
[appMenu addItem:quitMenuItem];
[appMenuItem setSubmenu:appMenu];
id appDelegate = [AppDelegate new];
[NSApp setDelegate: appDelegate];
[NSApp activateIgnoringOtherApps:YES];
[NSApp finishLaunching];
NSInteger style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
m_pWindow = [[NSWindow alloc] initWithContentRect:CGRectMake(0, 0, m_Config.screenWidth, m_Config.screenHeight) styleMask:style backing:NSBackingStoreBuffered defer:NO];
[m_pWindow setTitle:appName];
[m_pWindow makeKeyAndOrderFront:nil];
id winDelegate = [WindowDelegate new];
[m_pWindow setDelegate:winDelegate];
return result;
}
void CocoaApplication::Finalize()
{
[m_pWindow release];
}
void CocoaApplication::Tick()
{
NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:nil
inMode:NSDefaultRunLoopMode
dequeue:YES];
switch([(NSEvent *)event type])
{
case NSEventTypeKeyDown:
NSLog(@"Key Down Event Received!");
break;
default:
break;
}
[NSApp sendEvent:event];
[NSApp updateWindows];
[event release];
}
这里需要特别说明的就是我们去掉了[NSApp run],取而代之在Tick()当中使用了我们自己的EventLoop。这是为了保证我们的主循环在我们自己所写的main函数(最终会在驱动模块)当中。
然后在Platform/Darwin/CMakeLists.txt当中添加如下编译规则:
# MyGameEngineCocoa
add_executable(MyGameEngineCocoa MACOSX_BUNDLE
CocoaApplication.mm
AppDelegate.m
WindowDelegate.m
)
find_library(COCOA_LIBRARY Cocoa required)
target_link_libraries(MyGameEngineCocoa
Common
${OPENGEX_LIBRARY}
${OPENDDL_LIBRARY}
${XG_LIBRARY}
${COCOA_LIBRARY}
)
__add_xg_platform_dependencies(MyGameEngineCocoa)
执行build.sh之后,就会在./build/Platform/Darwin/MyGameEngineCocoa.app/Contents/MacOS/MyGameEngineCocoa当中生成我们的可执行文件。执行它就可以看到我们的窗体了:

看上去不错哦。然后让我们加入对于OpenGL的初始化代码。首先根据参考引用*4,创建两个新文件,GLView.{h,mm},从NSView派生出我们自己的View类:
GLView.h
#import <Cocoa/Cocoa.h>
@interface GLView : NSView
{
@private
NSOpenGLContext* _openGLContext;
NSOpenGLPixelFormat* _pixelFormat;
}
@end
#import "GLView.h"
#import <OpenGL/gl.h>
@implementation GLView
- (id)initWithFrame:(NSRect)frameRect pixelFormat:(NSOpenGLPixelFormat*)format
{
self = [super initWithFrame:frameRect];
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
[_openGLContext makeCurrentContext];
glClearColor(1,0,1,1);
glClear(GL_COLOR_BUFFER_BIT);
[_openGLContext flushBuffer];
}
- (instancetype)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
NSOpenGLPixelFormatAttribute attrs[] = {
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
NSOpenGLPFAColorSize,32,
NSOpenGLPFADepthSize,16,
NSOpenGLPFADoubleBuffer,
NSOpenGLPFAAccelerated,
0
};
_pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
if(_pixelFormat == nil)
{
NSLog(@"No valid matching OpenGL Pixel Format found");
return self;
}
_openGLContext = [[NSOpenGLContext alloc] initWithFormat:_pixelFormat shareContext:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_surfaceNeedsUpdate:)
name:NSViewGlobalFrameDidChangeNotification
object:self];
[_openGLContext makeCurrentContext];
return self;
}
- (void)lockFocus
{
[super lockFocus];
if([_openGLContext view]!= self)
{
[_openGLContext setView:self];
}
[_openGLContext makeCurrentContext];
}
- (void)update
{
[_openGLContext update];
}
- (void) _surfaceNeedsUpdate:(NSNotification*) notification
{
[self update];
}
@end
然后再添加两个文件,CocoaOpenGLApplication.{h,mm},从CocoaApplication派生出带有OpenGL Context(GLView)的应用类型,在CocoaApplication的初始化之后,将GLView的实例替换到窗口客户区:
int CocoaOpenGLApplication::Initialize()
{
int result = 0;
result = CocoaApplication::Initialize();
GLView* view = [[GLView alloc] initWithFrame:CGRectMake(0, 0, m_Config.screenWidth, m_Config.screenHeight)];
[m_pWindow setContentView:view];
return result;
}
最后是修改CMakeLists.txt。已经很长了,不赘述了。最后运行效果如题图。完成的代码在mac2分支当中。

参考引用
- Getting Started – OpenGL Wiki
- Programming OpenGL on macOS
- Creating a Cocoa application without NIB files
- OpenGL for macOS
- Drawing to a Window or View
- MACOSX_BUNDLE – CMake 3.0.2 Documentation
- AppKit | Apple Developer Documentation
- Demystifying NSApplication by recreating it
- Minimalist Cocoa programming
- Objective-C – Wikipedia
- Mixing Objective-C, C++ and Objective-C++: an Updated Summary