📜 ⬆️ ⬇️

Alawar Engine. Part two. Features cross-platform game engine

Good day! In the previous article , the general part of the process of creating games of the HOPA (Hidden Object Puzzle Game or “Hidden Objects”) game was considered. In this article, we will consider the principle of two-level software abstraction , which is a paradigm of the main platform-dependent components of our engine, and the general structure of the lower level engine . This approach allowed us to achieve flexibility in porting both the engine to new platforms and the games themselves from one platform to another. And also we managed to create:


The basis of the program part of the Alawar Engine includes 2 libraries: SF (Stargaze Framework library) and QE (Stargaze Quest Engine library). SF is the core of the entire system and contains almost the entire platform-specific implementation of the game. At the same time, the library has one common source source for all platforms. At the moment, SF operates under six platforms: Windows (XP, Vista, Windows 7), Mac OS X, iOS, Android, PS3 and Windows 8 (in development).

Quest Engine is an add-on to the Stargaze Framework that implements the logic of the game, created in the Quest Editor, and does not contain platform-specific code. Initially, QE was focused on games of the HOPA genre with a static set of static objects, but at the moment it is actively developing and allows you to implement games with a dynamic set of dynamic objects, such as Time Management (Resource management) and Tower Defense genres. Most platform-dependent software modules within SF have two levels of abstraction. Thanks to the above features, we managed to achieve a sufficiently high portability of our engine.

In general, the game project has a three-level structure and consists of user code, the Quest Engine library, the Stargaze Framework library, and additional libraries, such as a shopping library. With the right approach to implementation, custom code can be run in its original form, without additional changes and efforts on all platforms, which I wrote about at the beginning. By the way, our libraries can be connected to the game project both at the source level and at the level of compiled libraries. Using the example of the Stargaz Framework architecture, we will look at how to organize a cross-platform implementation of the game in order to understand how this possibility has been achieved.
')


At the lowest level of the Stargaze Framework are the system integration modules (platform-specific code), which are located in separate directories (win, android, etc.). Directories with the name of platforms contain exclusively platform-specific source code that is unique for a specific platform and do not contain source code that may be common to different platforms. For example, the implementation of calls to the graphics or audio subsystem of the current OS or color space conversion algorithms optimized for a specific architecture for video decoding. In addition, the platform-specific code in extreme cases is embedded in the main code through conditional compilation directives. This is done in cases where the allocation of a separate software abstraction is incommensurable with the size of the code, which consists in the directive of conditional compilation, for example, when connecting header files:

#if defined(__SF_WINDOWS) #include <Objbase.h> #elif defined(__SF_MAC) || defined(__SF_IPHONE) #include <uuid/uuid.h> #endif 

Due to this, the Stargaze Framework has a single growing branch and the same principles of work simultaneously for all platforms. In this case, the porting to the new platform begins in a separate branch, and then all changes merge into the current active branch.

Level above are the following modules:


At the next level, there are own subsystems of 2D graphics, audio and video, which actively use both the subsystems of the previous level and the modules of system integration:


In addition to the above subsystems, a number of intermediate ones can be distinguished:


At the topmost level is the GUI subsystem - a widget's own library containing a set of ready-made classic primitives, such as windows, buttons, input fields, checkboxes, radio buttons, etc. The integrating element is the widget manager, whose main tasks are to send user input messages to widgets, update the state of widgets and render them. In addition, the widget manager contains a gesture emulation module. This module has two main purposes: the processing of those gestures that are not implemented at the system level and the processing of custom gestures.

Let us consider in more detail the mechanisms of the application itself, 2D graphics, audio and video. In our framework, the base class of the CApplication application is highlighted, which contains almost no platform-specific code. This class describes the basic logic of the application, while the game developer creates a successor from this class and fills it with the required functionality. A platform-specific implementation of the mechanism of the application (initialization of the application, creation of the main window, event handling, etc.) is hidden in the classes inherited from CSystemIntegration:

 class CSystemIntegration { public: CSystemIntegration(); virtual ~CSystemIntegration(); virtual bool Init() = 0; virtual void Run() = 0; virtual void Stop() = 0; virtual void Shutdown() = 0; virtual bool EnsureSingleInstance() = 0; virtual bool ChangeScreenMode(bool _fullscreen, bool _32bpp, size_t _width, size_t _height) = 0; virtual bool GetOriginalDesktopDimentions(size_t &_width, size_t &_height) = 0; virtual EventInformation &GetCurrentEvent() = 0; virtual void DefaultWindowProc() = 0; virtual void GetWindowClientRect(misc::IntRect &_rc) = 0; virtual void AdjustClientRectToWindow(misc::IntRect &_rc) = 0; virtual void GetDesktopWindowedSpace(misc::IntRect &_rc) = 0; virtual void ScreenCoordsIntoClient(misc::IntVector& _pos) = 0; virtual void ClientCoordsIntoScreen(misc::IntVector& _pos) = 0; virtual void EnableSystemGestureRecognizer(int _recognizerType, bool _enable) {}; virtual void SetMouseCursorPos(const misc::IntVector& _pos) = 0; virtual void GetMouseCursorPos(misc::IntVector& _pos) = 0; virtual void SetSysCursor(gui::SysCursor _cursor, bool _show_now = true) = 0; virtual gui::SysCursor GetSysCursor() = 0; virtual void ShowSysCursor(bool _show = true) = 0; virtual bool IsSysCursorShown() = 0; protected: void AppUpdate(); void AppDraw(); void ActivateApp(bool _activate = true); void MinimizeApp(bool _minimized = true); }; 

In most cases, the game programmer only deals with high-level abstraction CApplication, which in turn uses CSystemIntegration. The interface of this class describes the general model of interaction with the system part of the application. The model assumes that the application has some output area (window), a queue of system messages (keyboard events, gestures, etc.) and the main work cycle. Although the direct implementation of class methods inherited from CSystemIntegration is not standardized, there are several conventions, for example, the main application work cycle on any platform should call AppUpdate () and AppDraw () methods sequentially. For example, for Windows and PS3 platforms, the implementation of the main cycles is as follows:

 void CStandaloneApplicationWindows::MessageCycle() { MSG msg; while (!m_EndModal) { while (!m_Stop && PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } if (m_Stop) break; AppUpdate(); AppDraw(); } m_EndModal = false; } void CStandaloneApplicationPS3::MessageCycle() { while (!m_EndModal) { if (g_quitRequested == false) { cellSysutilCheckCallback(); if ( cellPadUtilUpdate() ) { input::PointerApp::main(); _updatePad(); } if (m_Stop) break; AppUpdate(); AppDraw(); }else break; } m_EndModal = false; } 

Thus, to launch an application, it is enough for a game programmer to organize an entry point (on some platforms it is removed from SF) and write the following code (example for iOS):

 bool SFIPhoneMain() { static game::CGameApplication app; if (!app.Init()) return false; else return true; } void StartLoadGame() { sf::core::CApplication * app = sf::core::g_Application; app->SetMainWindow(new game::CMainMenuWindow()); app->Run(); } 

Where CGameApplication is the heir from CApplication, and CMainMenuWindow is the heir from CWindow defined by the programmer in the code of the game itself. The code illustrates that this model of a two-level software abstraction allows you to achieve the minimum cost of organizing the application, and as a result, the minimum headache associated with the transfer of the game to another platform. And in the context of several applications, this allows you to select an application entry point into a separate solution library.

The unified 2D graphics subsystem allows you to port the Stargaze Framework without any problems to platforms with different render machines, such as D3D9, D3D11, OpenGL ES (including 2.0), GCM. This versatility is achieved thanks to the CRenderer and CRenderDevice classes. The CRenderer class implements the top-level API — a single set of methods for working with 2D graphics, which fully covers the requirements for casual 2D games. For example:


It also stores the state stack of the render machine: blend color, transformation matrix, current texture and blend mode. The entire platform-specific implementation is hidden in the CRenderDevice classes (API of the lower level), which have the same type of interface:

 class CRenderDevice { public: CRenderDevice(); bool Init(); void Reset(); bool BeginScene(); bool EndScene(); void Render(RenderPrimitives _primitive, const RENDERVERTEX* const _verts, size_t _verts_count); void Render(RenderPrimitives _primitive, const void* const _verts, size_t _verts_count, DWORD _verts_fvf, DWORD _vertex_size); void Flush(); void SetTexture(DWORD _stage, IDirect3DTexture9* _texture); void SetTextureStageState(DWORD _stage, DWORD _state, DWORD _val); DWORD GetTextureStageState(DWORD _stage, DWORD _state) const; void SetBlendMode(BlendModes _blend_mode); void SetPixelShader(IDirect3DPixelShader9* _shader); void SetRenderTarget(IDirect3DTexture9* _texture); bool GetAvailableResolutions(std::list<Resolution> &_container); bool ClearRenderTarget(const Color& _color = 0); void ToggleHeavyRenderProfile(); private: … }; 

When calling a top-level API (CRenderer), for example, to draw a texture, CRenderer applies the current state of the render machine, recalculates the array of texture vertices on its own, and calls the CRenderDevice :: Render function. Any change in the state of the render machine inside CRenderDevice calls the Flush () function. For convenience, there are potential opportunities to use shaders and draw textures, but this is used very rarely in HOPA games.

For audio output, the CAudioManager class is used, which in turn uses any library that has an implementation on a specific platform (Bass, OpenAL, MultiStream, XAudio2, etc.). Within various platforms, this class can have both a two-level implementation, for example, Windows and MAC OS X, or a single-level implementation, such as iOS. This assumption is made due to the fact that on some platforms there is a convenient set of API for playing sounds. This class completely hides the details of playing sounds, providing access to sounds only by their identifiers and identifiers of groups of sounds. This allows you to minimize the time for "screwing" the sounds to the game. For example, running a specific track looks like this: sf :: core :: g_AudioManager :: Instance (). Play (“some_music”).

Perhaps the most problematic subsystem (but at the same time the most universal in terms of cross-platform implementation) is the video subsystem. This is due to the context of video usage in the game. For example, there can be four different video objects on a game scene, including an alpha channel, which leads to a decrease in fps in the game and an increased memory consumption. Currently, Windows, Android, MAC OS X and iOS use two cross-platform implementations based on Theora and WebM decoders. The latter is more preferable. The top-level API of this subsystem integrates the CVideo class, the interface of which, like CAudioManager, is quite simple.
The only exceptions are the two methods Update and Draw, which must be called to update the decoder and render the decoded texture, respectively. This allows us to display several different videos in one scene by layers. The lower level APIs are implemented by classes that are inherited from CVideo. These classes hide the methods of working with a specific decoder, as well as mixing normal video and alpha channel. This approach allowed us to minimize the cost of transferring video from one platform to another.

In the process of porting the Stargaze Framework library to various platforms, we concluded that the two-level model of abstraction of platform-dependent subsystems is more flexible. It allows to level the specifics of different platforms, providing a single principle of developing and porting the game.

Source: https://habr.com/ru/post/169897/


All Articles