从零开始手敲次世代游戏引擎(macOS特别篇 贰)

上一篇我们实现了在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;
    };
}

CocoaApplication.mm

#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

GLView.mm

#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分支当中。

参考引用

  1. Getting Started – OpenGL Wiki
  2. Programming OpenGL on macOS
  3. Creating a Cocoa application without NIB files
  4. OpenGL for macOS
  5. Drawing to a Window or View
  6. MACOSX_BUNDLE – CMake 3.0.2 Documentation
  7. AppKit | Apple Developer Documentation
  8. Demystifying NSApplication by recreating it
  9. Minimalist Cocoa programming
  10. Objective-C – Wikipedia
  11. Mixing Objective-C, C++ and Objective-C++: an Updated Summary

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

您正在使用您的 WordPress.com 账号评论。 登出 /  更改 )

Google photo

您正在使用您的 Google 账号评论。 登出 /  更改 )

Twitter picture

您正在使用您的 Twitter 账号评论。 登出 /  更改 )

Facebook photo

您正在使用您的 Facebook 账号评论。 登出 /  更改 )

Connecting to %s

%d 博主赞过: