📜 ⬆️ ⬇️

Framework in Marmalade (part 1)

In this series of articles, I will describe the development of a small Framework for creating 2D games using Marmalade . Marmalade provides an API for developing cross-platform applications, allowing you to build them, including, for Android and iOS. Work in Marmalade is quite comfortable, and its help system is accompanied by a large number of examples, but the development process itself is rather low-level. Using a ready-made Framework can make life much easier for a novice developer.

To better understand what we are going to do, let's start with a list of requirements. So, the framework we are developing should:



The development process itself will be consistently described in this and subsequent articles. Below is a class diagram:
')
image

Main interfaces and classes:



We will start the development by building a mkb file, which is the description by which Marmalade will build a C ++ project.

mf.mkb
#!/usr/bin/env mkb options { } subprojects { iw2d } includepath { ./source/Main ./source/Common ./source/Scene } files { [Main] (source/Main) Main.cpp Main.h Desktop.cpp Desktop.h [Common] (source/Common) IObject.h IScreenObject.h ISprite.h ISpriteOwner.h AbstractScreenObject.h AbstractScreenObject.cpp AbstractSpriteOwner.h AbstractSpriteOwner.cpp [Scene] (source/Scene) Scene.cpp Scene.h Background.cpp Background.h Sprite.cpp Sprite.h [Data] (data) } assets { (data) background.png sprite.png (data-ram/data-gles1, data) } 



For now, empty sections can be ignored (we will need them later). The subprojects section describes the subprojects that we use (at present, this is only the iw2d Marmalade subsystem, which will allow us to work with 2D graphics). The includepath, as the name implies, lists the names of directories containing h-files. The files section describes the source files (the name in square brackets defines the name of the folder in the MSVC project, and the path in parentheses shows where the folder is located on the disk). The assets section describes the resources used by the application.

Next, we will create presets for h- and cpp-files in advance, placing them in the appropriate project folders, then run the MKB file for execution. If everything is done correctly, Microsoft Visual Studio will open, in which we will see our project:

image

The main loop of our application will be located in Main.cpp:

Main.cpp
 #include "Main.h" #include "s3e.h" #include "Iw2D.h" #include "IwGx.h" #include "Desktop.h" #include "Scene.h" #include "Background.h" #include "Sprite.h" void init() { // Initialise Mamrlade graphics system and Iw2D module IwGxInit(); Iw2DInit(); // Set the default background clear colour IwGxSetColClear(0x0, 0x0, 0x0, 0); desktop.init(); } void release() { desktop.release(); Iw2DTerminate(); IwGxTerminate(); } int main() { init(); { Scene scene; new Background(&scene, "background.png", 1); new Sprite(&scene, "sprite.png", 122, 100, 2); desktop.setScene(&scene); int32 duration = 1000 / 25; // Main Game Loop while (!s3eDeviceCheckQuitRequest()) { // Update keyboard system s3eKeyboardUpdate(); if ((s3eKeyboardGetState(s3eKeyAbsBSK) & S3E_KEY_STATE_DOWN) == S3E_KEY_STATE_DOWN) break; // Update desktop.update(s3eTimerGetMs()); // Clear the screen IwGxClear(IW_GX_COLOUR_BUFFER_F | IW_GX_DEPTH_BUFFER_F); // Refresh desktop.refresh(); // Show the surface Iw2DSurfaceShow(); // Yield to the opearting system s3eDeviceYield(duration); } } release(); return 0; } 



This is fairly generic code for Marmalade projects. In init, all subsystems with which we will work are initialized, then the scene is created and its contents, which we will display and transferred to the desktop as the main scene (this is done in the operator block, in order to ensure that the scene object is removed before the release function is called) .

In the main application loop, we check the exit condition by pressing the “s3eKeyAbsBSK” button (we will work with the keyboard in subsequent articles), then update the desktop, passing it the current timestamp value, clear the screen, call the desktop redraw, display the changes on the screen by calling Iw2DSurfaceShow and then transfer control to the operating system by calling s3eDeviceYield. At the end of the main loop, we clear the resources in the release function.

The Desktop class provides us with interaction with the device screen:

Desktop.h
 #ifndef _DESKTOP_H_ #define _DESKTOP_H_ #include "Scene.h" class Desktop { private: int width, height; Scene* currentScene; public: void init(); void release() {} void update(uint64 timestamp); void refresh(); int getWidth() const {return width;} int getHeight() const {return height;} Scene* getScene() {return currentScene;} void setScene(Scene* scene); }; extern Desktop desktop; #endif // _DESKTOP_H_ 



Screen sizes are obtained using Iw2DGetSurfaceWidth and Iw2DGetSurfaceHeight calls.

Desktop.cpp
 #include "Desktop.h" #include "Iw2D.h" Desktop desktop; void Desktop::init() { width = Iw2DGetSurfaceWidth(); height = Iw2DGetSurfaceHeight(); setScene(NULL); } void Desktop::setScene(Scene* scene) { if (scene != NULL) { scene->init(); } currentScene = scene; } void Desktop::update(uint64 timestamp) { if (currentScene != NULL) { currentScene->update(timestamp); } } void Desktop::refresh() { if (currentScene != NULL) { currentScene->refresh(); } } 


Create the necessary interfaces, in accordance with the previously developed architecture.

IObject.h
 #ifndef _IOBJECT_H_ #define _IOBJECT_H_ #include "s3e.h" class IObject { public: virtual ~IObject() {} virtual bool isBuzy() = 0; virtual int getState() = 0; virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL) = 0; virtual bool sendMessage(int msg, int x, int y) = 0; virtual void update(uint64 timestamp) = 0; virtual void refresh() = 0; }; #endif // _IOBJECT_H_ 


IScreenObject.h
 #ifndef _ISCREENOBJECT_H_ #define _ISCREENOBJECT_H_ #include "s3e.h" #include "IObject.h" class IScreenObject: public IObject { public: virtual int getXPos() = 0; virtual int getYPos() = 0; virtual int getWidth() = 0; virtual int getHeight() = 0; }; #endif // _ISCREENOBJECT_H_ 


ISprite.h
 #ifndef _ISPRITE_H_ #define _ISPRITE_H_ #include "Locale.h" #include "Iw2D.h" #include "IwGx.h" class ISprite { public: virtual void addImage(const char* res, int state = 0) = 0; virtual CIw2DImage* getImage(int state = 0) = 0; }; #endif // _ISPRITE_H_ 


ISpriteOwner.h
 #ifndef _ISPRITEOWNER_H_ #define _ISPRITEOWNER_H_ #include "IObject.h" #include "AbstractScreenObject.h" class ISpriteOwner: public IObject { public: virtual void addSprite(AbstractScreenObject* sprite, int zOrder) = 0; virtual bool setZOrder(AbstractScreenObject* sprite, int z) = 0; virtual int getDesktopWidth() = 0; virtual int getDesktopHeight() = 0; virtual int getXSize(int xSize) = 0; virtual int getYSize(int ySize) = 0; virtual int getXPos(int x) = 0; virtual int getYPos(int y) = 0; }; #endif // _ISPRITEOWNER_H_ 



In the helper class AbstractScreenObject, we will maintain a reference count and store the sprite layout parameters:

AbstractScreenObject.h
 #ifndef _ABSTRACTSCREENOBJECT_H_ #define _ABSTRACTSCREENOBJECT_H_ #include <string> #include "Iw2D.h" #include "IScreenObject.h" using namespace std; class AbstractScreenObject: public IScreenObject { private: static int idCounter; int id; int usageCounter; protected: virtual bool init(); CIw2DAlphaMode alpha; int xPos, yPos, angle; int xDelta, yDelta; bool isVisible; bool isInitialized; public: AbstractScreenObject(int x, int y); virtual ~AbstractScreenObject() {} int getId() const {return id;} void incrementUsage(); bool decrementUsage(); virtual int getXPos() {return xPos + xDelta;} virtual int getYPos() {return yPos + yDelta;} virtual int getWidth() {return 0;} virtual int getHeight() {return 0;} virtual bool isBackground() {return false;} virtual bool isBuzy() {return false;} int getAngle() const {return angle;} void move(int x = 0, int y = 0); void setXY(int x = 0, int y = 0); void clearXY(); void setAngle(int a) {angle = a;} void setAlpha(CIw2DAlphaMode a) {alpha = a;} bool setState(int state) {return false;} }; #endif // _ABSTRACTSCREENOBJECT_H_ 


AbstractScreenObject.cpp
 #include "AbstractScreenObject.h" #include "Desktop.h" int AbstractScreenObject::idCounter = 0; AbstractScreenObject::AbstractScreenObject(int x, int y): xPos(x), alpha(IW_2D_ALPHA_NONE),yPos(y), angle(0), xDelta(0), yDelta(0), isVisible(true), isInitialized(false), usageCounter(0) { id = ++idCounter; } bool AbstractScreenObject::init() { bool r = !isInitialized; isInitialized = true; return r; } void AbstractScreenObject::incrementUsage() { usageCounter++; } bool AbstractScreenObject::decrementUsage() { usageCounter--; return (usageCounter == 0); } void AbstractScreenObject::move(int x, int y) { xDelta += x; yDelta += y; } void AbstractScreenObject::setXY(int x, int y) { xPos = x; yPos = y; } void AbstractScreenObject::clearXY() { xDelta = 0; yDelta = 0; } 



Go to the implementation of the sprites.

Sprite.h
 #ifndef _SPRITE_H_ #define _SPRITE_H_ #include "AbstractScreenObject.h" #include "ISprite.h" #include "ISpriteOwner.h" #include "Locale.h" class Sprite: public AbstractScreenObject , public ISprite { protected: ISpriteOwner* owner; CIw2DImage* img; public: Sprite(ISpriteOwner* owner, int x, int y , int zOrder = 0); Sprite(ISpriteOwner* owner, const char* res, int x, int y, int zOrder = 0); virtual ~Sprite(); virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL); virtual bool sendMessage(int msg, int x, int y) {return false;} virtual void update(uint64 timestamp) {} virtual void refresh(); virtual void addImage(const char*res, int state = 0); virtual CIw2DImage* getImage(int id = 0); virtual int getState() {return 0;} virtual int getWidth(); virtual int getHeight(); }; #endif // _SPRITE_H_ 


Sprite.cpp
 #include "Sprite.h" #include "Locale.h" Sprite::Sprite(ISpriteOwner* owner, int x, int y, int zOrder): AbstractScreenObject(x, y) , owner(owner) , img(NULL) { owner->addSprite((AbstractScreenObject*)this, zOrder); } Sprite::Sprite(ISpriteOwner* owner, const char* res, int x, int y, int zOrder): AbstractScreenObject(x, y) , owner(owner) , img(NULL) { addImage(res, 0); owner->addSprite((AbstractScreenObject*)this, zOrder); } Sprite::~Sprite() { if (img != NULL) { delete img; } } bool Sprite::sendMessage(int msg, uint64 timestamp, void* data) { return owner->sendMessage(msg, timestamp, data); } void Sprite::addImage(const char*res, int state) { img = Iw2DCreateImage(res); } CIw2DImage* Sprite::getImage(int id) { return img; } int Sprite::getWidth() { CIw2DImage* img = getImage(getState()); if (img != NULL) { return img->GetWidth(); } else { return 0; } } int Sprite::getHeight() { CIw2DImage* img = getImage(getState()); if (img != NULL) { return img->GetHeight(); } else { return 0; } } void Sprite::refresh() { init(); CIw2DImage* img = getImage(getState()); if (isVisible && (img != NULL)) { CIwMat2D m; m.SetRot(getAngle()); m.ScaleRot(IW_GEOM_ONE); m.SetTrans(CIwSVec2(owner->getXSize(owner->getXPos(getXPos())), owner->getYSize(owner->getYPos(getYPos())))); Iw2DSetTransformMatrix(m); Iw2DSetAlphaMode(alpha); Iw2DDrawImage(img, CIwSVec2(0, 0), CIwSVec2(owner->getXSize(getWidth()), owner->getYSize(getHeight()))); } } 



Here you should pay attention to the addImage methods (loading a picture from a resource) and refresh (displaying a picture on the screen). In the latter, with a sequence of calls SetRot, ScaleRot, SetTrans better not to experiment. By transferring the corresponding parameters to these calls, you can rotate and scale the image, as well as transfer it relative to the origin.

In the SetTrans and Iw2DrawImage calls, we use the getXSize and getYSize methods to convert logical coordinates to screen coordinates. A little later, we will consider them. Note the important difference between ScaleRot and the use of the third (optional parameter) Iw2DrawImage. If the first of them allows you to perform an affine transformation, a scaling image, then in the second case, we can scale the original image independently along the X and Y axes (to bring the desired aspect ratio).

The Background class is Sprite's successor and overrides the refresh method. The screen image size is not calculated based on the data provided by the owner, but is taken directly from the desktop dimensions.

Background.h
 #ifndef _BACKGROUND_H_ #define _BACKGROUND_H_ #include "Sprite.h" #include "Locale.h" class Background: public Sprite { public: Background(ISpriteOwner* owner, const char* res, int zOrder); virtual bool isBackground() {return true;} virtual void refresh(); }; #endif // _BACKGROUND_H_ 



Background.cpp
 #include "Background.h" Background::Background(ISpriteOwner* owner, const char* res, int zOrder): Sprite(owner, res, 0, 0, zOrder) {} void Background::refresh() { CIwMat2D m; m.SetRot(0); m.ScaleRot(IW_GEOM_ONE); m.SetTrans(CIwSVec2(0, 0)); Iw2DSetTransformMatrix(m); Iw2DSetAlphaMode(alpha); Iw2DDrawImage(img, CIwSVec2(0, 0), CIwSVec2(owner->getDesktopWidth(), owner->getDesktopHeight())); } 



AbstractSpriteOwner manages the storage of sprites and leads the Z-sequence of their display on the screen.

AbstractSpriteOwner.h
 #ifndef _ABSTRACTSPRITEOWNER_H_ #define _ABSTRACTSPRITEOWNER_H_ #include <map> #include "IObject.h" #include "ISpriteOwner.h" #include "AbstractScreenObject.h" using namespace std; class AbstractSpriteOwner: public ISpriteOwner { protected: multimap<int, AbstractScreenObject*> zOrder; public: AbstractSpriteOwner(); virtual ~AbstractSpriteOwner(); virtual void addSprite(AbstractScreenObject* sprite, int z); virtual bool setZOrder(AbstractScreenObject* sprite, int z); virtual int getDesktopWidth(); virtual int getDesktopHeight(); virtual int getState() {return 0;} virtual void update(uint64 timestamp); virtual void refresh(); virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL) {return false;} virtual bool sendMessage(int msg, int x, int y); typedef multimap<int, AbstractScreenObject*>::iterator ZIter; typedef multimap<int, AbstractScreenObject*>::reverse_iterator RIter; typedef pair<int, AbstractScreenObject*> ZPair; }; #endif // _ABSTRACTSPRITEOWNER_H_ 


AbstractSpriteOwner.cpp
 #include "AbstractSpriteOwner.h" #include "Desktop.h" #include "ISprite.h" AbstractSpriteOwner::AbstractSpriteOwner(): zOrder() {} AbstractSpriteOwner::~AbstractSpriteOwner() { for (ZIter p = zOrder.begin(); p != zOrder.end(); ++p) { if (p->second->decrementUsage()) { delete p->second; } } } void AbstractSpriteOwner::addSprite(AbstractScreenObject* sprite, int z) { sprite->incrementUsage(); zOrder.insert(ZPair(z, sprite)); } bool AbstractSpriteOwner::setZOrder(AbstractScreenObject* sprite, int z) { for (ZIter p = zOrder.begin(); p != zOrder.end(); ++p) { if (p->second == sprite) { zOrder.erase(p); zOrder.insert(ZPair(z, sprite)); return true; } } return false; } int AbstractSpriteOwner::getDesktopWidth() { return desktop.getWidth(); } int AbstractSpriteOwner::getDesktopHeight() { return desktop.getHeight(); } void AbstractSpriteOwner::update(uint64 timestamp) { for (ZIter p = zOrder.begin(); p != zOrder.end(); ++p) { p->second->update(timestamp); } } void AbstractSpriteOwner::refresh() { for (ZIter p = zOrder.begin(); p != zOrder.end(); ++p) { p->second->refresh(); } } bool AbstractSpriteOwner::sendMessage(int msg, int x, int y) { for (RIter p = zOrder.rbegin(); p != zOrder.rend(); ++p) { if (p->second->isBackground()) continue; if (p->second->sendMessage(msg, x, y)) { return true; } } return false; } 



The heir to AbstractSpriteOwner is Scene:

Scene.h
 #ifndef _SCENE_H_ #define _SCENE_H_ #include "s3eKeyboard.h" #include "AbstractSpriteOwner.h" #include "AbstractScreenObject.h" using namespace std; class Scene: public AbstractSpriteOwner { private: AbstractScreenObject* background; bool isInitialized; public: Scene(); virtual bool init(); int getXSize(int xSize); int getYSize(int ySize); virtual int getXPos(int x) {return x;} virtual int getYPos(int y) {return y;} virtual void refresh(); virtual void update(uint64 timestamp); virtual bool isBuzy() {return false;} virtual bool sendMessage(int id, int x, int y); }; #endif // _SCENE_H_ 


Scene.cpp
 #include "Scene.h" #include "Desktop.h" Scene::Scene(): AbstractSpriteOwner() , isInitialized(false) , background(NULL) {} bool Scene::init() { bool r = !isInitialized; isInitialized = true; return r; } int Scene::getXSize(int xSize) { if (background != NULL) { return (getDesktopWidth() * xSize) / background->getWidth(); } return xSize; } int Scene::getYSize(int ySize) { if (background != NULL) { return (getDesktopHeight() * ySize) / background->getHeight(); } return ySize; } void Scene::refresh() { init(); if (background == NULL) { for (ZIter p = zOrder.begin(); p != zOrder.end(); ++p) { if (p->second->isBackground()) { background = p->second; break; } } } AbstractSpriteOwner::refresh(); } void Scene::update(uint64 timestamp) { AbstractSpriteOwner::update(timestamp); } bool Scene::sendMessage(int id, int x, int y) { if (AbstractSpriteOwner::sendMessage(id, x, y)) { return true; } if (background != NULL) { return background->sendMessage(id, x, y); } return false; } 



All is ready. Run our application for execution ... and get the error:

image

The fact is that dynamic memory, on mobile platforms, is an extremely valuable resource and is carefully taken into account in Marmalade. Let's make the necessary changes to the app.icf settings file (at the same time we will fix the landscape orientation of the screen):

app.icf
 [S3E] DispFixRot=FixedLandscape MemSize=70000000 MemSizeDebug=70000000 



At this our today's ordeals end and, running the application to run again, we see the sprite displayed on top of the background image. In the next article we will talk about event handling.

Source code of the current version is here.

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


All Articles