Sprite Map

9 posts / 0 new
Last post
anol
anol's picture
Offline
Mobile Wizard
Joined: 11 Dec 2009
Posts:
Sprite Map

I've made a little class that might be useful to someone else. And maybe someone has can suggest some improvements? Perhaps I have missed something that is already included?

It is a sprite map, to put graphics for more than one sprite in one image. It is useful for animation for instance. The source is a complete runnable example.

main.cpp:

#include "MAHeaders.h"

#include "ma.h"
#include <MAUtil/Moblet.h>

#include <MAUI/Screen.h>
#include <MAUI/Widget.h>

using namespace MAUtil;
using namespace MAUI;

class SpriteMap
{
public:
	SpriteMap(MAHandle image, int columns, int rows=1) :
		image(image)
	{
		MAExtent e = maGetImageSize(image);
		width = EXTENT_X(e) / columns;
		height = EXTENT_Y(e) / rows;
		srcRect.width = width;
		srcRect.height = height;
	}

	void drawSprite(int x, int y, int column, int row=0, int transform=TRANS_NONE) {
		srcRect.left = column * width;
		srcRect.top = row * height;
		dstPoint.x = x - (width >> 1);
		dstPoint.y = y - (height >> 1);
		maDrawImageRegion(image, &srcRect, &dstPoint, transform);
	}
private:
	MAHandle image;
	int width;
	int height;
	MARect srcRect;
	MAPoint2d dstPoint;
};

class MyWidget : public Widget, public TimerListener {
public:
	MyWidget(int x, int y, int width, int height, Widget* parent) :
		Widget(x, y, width, height, parent),
		//backColor(Innocent, why not?
		spriteMap(STAR_SMAP, 8, 2),
		startMs(maGetMilliSecondCount())
	{
			this->setBackgroundColor(0x000000);
			//this->backColor = 0x000000; //This works
	}

	void drawWidget()
	{
		spriteMap.drawSprite(100, 100, (time>>6) & 7, 0);
		spriteMap.drawSprite(100, 100, (-time>>7) & 7, 1);
	}

	void start()
	{
		MAUtil::Environment::getEnvironment().addTimer(this, 20, -1); // 50Hz, forever
	}

	void tick(int dt)
	{
		time += dt;
	}

	void runTimerEvent() {
		int now = maGetMilliSecondCount();
		int dt = now - startMs;
		startMs = now;
		tick(dt);
		requestRepaint();
	}


private:
	SpriteMap spriteMap;
	int startMs;
	int time;
};


class MyScreen : public Screen {
public:
	MyScreen() :
	myWidget(0, 0, 0, 0, NULL)
	{
		myWidget.start();
		setMain(&myWidget);
	}
	
	~MyScreen() {
	}
private:
	MyWidget myWidget;
};

class MAUIMoblet : public Moblet {
public:
	MAUIMoblet():
	screen()
	{
		screen.show();
	}

	void keyPressEvent(int keyCode) {
		// todo: handle key presses
	}

	void keyReleaseEvent(int keyCode) {
		// todo: handle key releases
	}

	~MAUIMoblet() {
	}
private:
	MyScreen screen;
};

extern "C" int MAMain() {
	Moblet::run(new MAUIMoblet());
	return 0;
};

gfx.lst:

.res STAR_SMAP
.image "smap.png"

I just noticed that the image is not very explaining, since it is a white image with different alphas. You'll have to look at it with a different background colour somehow.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
Alex Jonsson
alexj's picture
Offline
Mobile Conjurer
Joined: 8 Sep 2009
Posts:

Very cool indeed!

I guess you'll get more comments soon for sure

best
Alex

And when I looked at the "starmap" off a gray background, it looked something like this (zoomed in):

star_gray.png
Fredrik Eldh
Fredrik's picture
Offline
Mobile Sorcerer
Joined: 16 Feb 2009
Posts:

Cute. Smile

offe
offe's picture
Offline
Joined: 7 Jan 2010
Posts:

I didn't mean to confuse anyone. By mistake I posted the first message with anols user, he was logged here in at the time. Sorry about that.

Anyway. I continued to play around a bit, and added some particles. Now it is kind of an interactive sparkling wand effect.

[attachment=0]sparkle.png[/attachment]

It is more fun when it is animated and you play around with it with the mouse (or touchscreen on a device), so please try it out.

You'll need the same two files as before (the resource file and the png).

Any comments on the code or on performance on actual devices are greatly appreciated.

/* Copyright (C) 2010 Oscar Lindberg

This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License, version 2, as published by
the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License
along with this program; see the file COPYING.  If not, write to the Free
Software Foundation, 59 Temple Place - Suite 330, Boston, MA
02111-1307, USA.
*/

#include "MAHeaders.h"

#include "ma.h"
#include <MAUtil/Moblet.h>
#include <MAUtil/Environment.h>

#include <MAUI/Screen.h>
#include <MAUI/Widget.h>

using namespace MAUtil;
using namespace MAUI;

unsigned long randx = 0x01234567;
unsigned int rand()
{
	randx *= 1103515245;
	randx += 12345;
	return (randx >> 16) & 0xffff;
}

class SpriteMap
{
public:
	SpriteMap(MAHandle image, int columns, int rows=1) :
		image(image)
	{
		MAExtent e = maGetImageSize(image);
		width = EXTENT_X(e) / columns;
		height = EXTENT_Y(e) / rows;
		srcRect.width = width;
		srcRect.height = height;
	}

	void drawSprite(int x, int y, int column, int row=0, int transform=TRANS_NONE) {
		srcRect.left = column * width;
		srcRect.top = row * height;
		dstPoint.x = x - (width >> 1);
		dstPoint.y = y - (height >> 1);
		maDrawImageRegion(image, &srcRect, &dstPoint, transform);
	}
private:
	MAHandle image;
	int width;
	int height;
	MARect srcRect;
	MAPoint2d dstPoint;
};

// To keep destructors empty
class PointerListenerToken
{
public:
	PointerListenerToken(PointerListener* pl) : pl(pl)
	{Environment::getEnvironment().addPointerListener(pl);}

	~PointerListenerToken()
	{Environment::getEnvironment().addPointerListener(pl);}
private:
	PointerListener* pl;
};

// To keep destructors empty
class TimerListenerToken
{
public:
	TimerListenerToken(TimerListener* tl, int period_ms, int numTimes=-1) : tl(tl)
	{MAUtil::Environment::getEnvironment().addTimer(tl, period_ms, numTimes);}

	~TimerListenerToken()
	{MAUtil::Environment::getEnvironment().removeTimer(tl);}
private:
	TimerListener* tl;
};

class Particle {
public:
	Particle(SpriteMap& spriteMap,
			float x, float y,
			float vx, float vy,
			int rotation_ms,
			int sparkling_period, float noshow_part,
			int row, int lifetime) :
		spriteMap(&spriteMap),
		x(x), y(y),
		vx(vx), vy(vy),
		rotation_ms(rotation_ms),
		sparkling_period(sparkling_period), noshow_interval((int)(sparkling_period*noshow_part)),
		row(row), lifetime(lifetime), t(Innocent
		{}

	Particle() {}

	void tick(int dt) {
		t += dt;
		float dt_s = (float)dt / 1000.0;
		vy += 3.0;
		x += vx * dt_s;
		y += vy * dt_s;
	}

	void draw()
	{
		if (t % sparkling_period > noshow_interval)
		{
			spriteMap->drawSprite((int)x, (int)y, (8*t / rotation_ms) & 7, row);
		}
	}

	bool dead()
	{
		return t >= lifetime;
	}
private:
	SpriteMap* spriteMap;
	float x;
	float y;
	float vx;
	float vy;
	int rotation_ms;
	int sparkling_period;
	int noshow_interval;
	int row;
	int lifetime;
	int t;
};

/* A bag is like a vector, but elements may move around. Also known by the name multiset.
  Initiation: O(n)
  Adding elements: O(1)
  Removing elements O(1) (vectors are O(n))
  Accessing elements O(1)

  This implementation has a fixed max size.
  It will silently ignore calls to add if it is at maximum capacity.
 */
template<typename Type>
class Bag
{
public:
	Bag(int maxsize)
	: elements(new Type[maxsize]), indexes(new int[maxsize]), mCapacity(maxsize), mSize(Innocent
	{
		for (int i = 0; i < maxsize; ++i) {
			indexes[i] = i;
		}
	}

	~Bag()
	{
		delete[] elements;
		delete[] indexes;
	}

	void add(const Type& val) {
		if (!isFull())
		{
			elements[indexes[mSize++]] = val;
		}
	}

	void remove(int index) {
		int tmp = indexes[--mSize];
		indexes[mSize] = indexes
; indexes
= tmp; } Type& operator[](int index) { return elements[indexes
]; } int size() { return mSize; } int capacity() { return mCapacity; } bool isFull() { return (mSize == mCapacity); } void callAll(void (Type::*f)()) { for (int i=0; i < mSize; ++i) { (elements[indexes[i]].*f)(); } } // Calls a single arity member function on all elements template<typename A> void callAll1(void (Type::*f)(A), A arg1) { for (int i=0; i < mSize; ++i) { (elements[indexes[i]].*f)(arg1); } } void removeIf(bool (Type::*pred)()) { int i=0; while(i < mSize) { if((elements[indexes[i++]].*pred)()) { remove(--i); } } } private: Type* elements; int* indexes; int mCapacity; int mSize; }; class MyWidget : public Widget, public TimerListener, PointerListener { public: MyWidget(int x=0, int y=0, int width=0, int height=0, Widget* parent=NULL) : Widget(x, y, width, height, parent), spriteMap(STAR_SMAP, 8, 3), particleBag(100), startMs(maGetMilliSecondCount()), pointerIsDown(false), pointerListenerToken(this), timerListenerToken(this, 20) // 50 Hz { this->setBackgroundColor(0x000000); } ~MyWidget() { } void drawWidget() { particleBag.callAll(&Particle::draw); } void tick(int dt) { time += dt; if (pointerIsDown) { float dt_s = (float)dt / 1000.0; float mystery_factor = 0.5; // This should be 1.0 in my mind. Don't understand. float pointerVelX = mystery_factor * (newPointerPos.x - lastPointerPos.x) / dt_s; float pointerVelY = mystery_factor * (newPointerPos.y - lastPointerPos.y) / dt_s; lastPointerPos = newPointerPos; addParticle(lastPointerPos, pointerVelX, pointerVelY); } particleBag.callAll1(&Particle::tick, dt); particleBag.removeIf(&Particle::dead); } void runTimerEvent() { int now = maGetMilliSecondCount(); int dt = now - startMs; startMs = now; tick(dt); requestRepaint(); } void putRandomCircleCoordinates(int r, int& x, int& y) { int r2 = r*r; do { x = rand() % (2*r) - r; y = rand() % (2*r) - r; } while ((x*x + y*y) >= r2); } void addParticle(MAPoint2d p, float pointerVelX, float pointerVelY) { int dx; int dy; int dvx; int dvy; putRandomCircleCoordinates(20, dx, dy); // Spread out the origin putRandomCircleCoordinates(20, dvx, dvy); // Disturb the initial velocity int rotation_ms = ((rand() & 2)-1)*(rand() % 400 + 100); // -0.5 to -0.1 and 0.1 to 0.5 Particle particle(spriteMap, p.x + dx, p.y + dy, pointerVelX + dvx, pointerVelY + dvy, rotation_ms, rand() % 750 + 250, // Sparkle interval between 0.25 and 1 secs 0.2, // Not shown (because of sparkle) 20% of the time rand() % 2, // Row of graphics in sprite map rand() % 4000 + 1000); if (!particleBag.isFull()) { particleBag.add(particle); } else { // Replace random sprite particleBag[rand() % particleBag.capacity()] = particle; } } void pointerPressEvent(MAPoint2d p) { pointerIsDown = true; lastPointerPos = p; newPointerPos = p; } void pointerMoveEvent(MAPoint2d p) { newPointerPos = p; }; void pointerReleaseEvent(MAPoint2d p) { pointerIsDown = false; }; private: SpriteMap spriteMap; Bag<Particle> particleBag; int startMs; bool pointerIsDown; MAPoint2d newPointerPos; MAPoint2d lastPointerPos; int time; PointerListenerToken pointerListenerToken; TimerListenerToken timerListenerToken; }; class MyScreen : public Screen { public: MyScreen() { setMain(&myWidget); } ~MyScreen() { } private: MyWidget myWidget; }; class ParticleMoblet : public Moblet { public: ParticleMoblet() { screen.show(); } ~ParticleMoblet() { } private: MyScreen screen; }; extern "C" int MAMain() { ParticleMoblet moblet; Moblet::run(&moblet); return 0; };
sparkle.png
Sam Pickard
rival's picture
Offline
Mobile Archmage
Joined: 19 Mar 2009
Posts:

This looks very cool. My only suggestion is that as you've made it part of a MAUI widget then it needs to respect the transformation stack. Widgets can be moved by other widgets, for instance if you put your widget in a scrolling list box, then the place on screen where it has to draw may be changed.

You can do this easily by calling Gfx_drawImageRegion (Handle image, const MARect *srcRect, const MAPoint2d *dstPoint, int transformMode)
instead of maDrawImageRegion(image, &srcRect, &dstPoint, transform); in the drawSprite method.

Then again, I'm not the biggest expert on this on here, so someone else tell me if I've got this wrong.

offe
offe's picture
Offline
Joined: 7 Jan 2010
Posts:

Thank you for your suggestion. I now use Gfx_drawImageRegion instead, so now I'm not drawing outside my widget.

I also had to adjust the pointer events' coordinates with the widget's absolute position (in "bounds.x" and "bounds.y").

/* Copyright (C) 2010 Oscar Lindberg

This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License, version 2, as published by
the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License
along with this program; see the file COPYING.  If not, write to the Free
Software Foundation, 59 Temple Place - Suite 330, Boston, MA
02111-1307, USA.
*/

#include "MAHeaders.h"

#include "ma.h"
#include <MAUtil/Moblet.h>
#include <MAUtil/Environment.h>
#include <MAUtil/Graphics.h>

#include <MAUI/Screen.h>
#include <MAUI/Widget.h>
#include <MAUI/Label.h>

using namespace MAUtil;
using namespace MAUI;

unsigned long randx = 0x01234567;
unsigned int rand()
{
	randx *= 1103515245;
	randx += 12345;
	return (randx >> 16) & 0xffff;
}

class SpriteMap
{
public:
	SpriteMap(MAHandle image, int columns, int rows=1) :
		image(image)
	{
		MAExtent e = maGetImageSize(image);
		width = EXTENT_X(e) / columns;
		height = EXTENT_Y(e) / rows;
		srcRect.width = width;
		srcRect.height = height;
	}

	void drawSprite(int x, int y, int column, int row=0, int transform=TRANS_NONE) {
		srcRect.left = column * width;
		srcRect.top = row * height;
		dstPoint.x = x - (width >> 1);
		dstPoint.y = y - (height >> 1);
		//maDrawImageRegion(image, &srcRect, &dstPoint, transform);
		Gfx_drawImageRegion(image, &srcRect, &dstPoint, transform);
	}
private:
	MAHandle image;
	int width;
	int height;
	MARect srcRect;
	MAPoint2d dstPoint;
};

// To keep destructors empty
class PointerListenerToken
{
public:
	PointerListenerToken(PointerListener* pl) : pl(pl)
	{Environment::getEnvironment().addPointerListener(pl);}

	~PointerListenerToken()
	{Environment::getEnvironment().addPointerListener(pl);}
private:
	PointerListener* pl;
};

// To keep destructors empty
class TimerListenerToken
{
public:
	TimerListenerToken(TimerListener* tl, int period_ms, int numTimes=-1) : tl(tl)
	{MAUtil::Environment::getEnvironment().addTimer(tl, period_ms, numTimes);}

	~TimerListenerToken()
	{MAUtil::Environment::getEnvironment().removeTimer(tl);}
private:
	TimerListener* tl;
};

class Particle {
public:
	Particle(SpriteMap& spriteMap,
			float x, float y,
			float vx, float vy,
			int rotation_ms,
			int sparkling_period, float noshow_part,
			int row, int lifetime) :
		spriteMap(&spriteMap),
		x(x), y(y),
		vx(vx), vy(vy),
		rotation_ms(rotation_ms),
		sparkling_period(sparkling_period), noshow_interval((int)(sparkling_period*noshow_part)),
		row(row), lifetime(lifetime), t(Innocent
		{}

	Particle() {}

	void tick(int dt) {
		t += dt;
		float dt_s = (float)dt / 1000.0;
		vy += 3.0;
		x += vx * dt_s;
		y += vy * dt_s;
	}

	void draw()
	{
		if (t % sparkling_period > noshow_interval)
		{
			spriteMap->drawSprite((int)x, (int)y, (8*t / rotation_ms) & 7, row);
		}
	}

	bool dead()
	{
		return t >= lifetime;
	}
private:
	SpriteMap* spriteMap;
	float x;
	float y;
	float vx;
	float vy;
	int rotation_ms;
	int sparkling_period;
	int noshow_interval;
	int row;
	int lifetime;
	int t;
};

/* A bag is like a vector, but elements may move around. Also known by the name multiset.
  Initiation: O(n)
  Adding elements: O(1)
  Removing elements O(1) (vectors are O(n))
  Accessing elements O(1)

  This implementation has a fixed max size.
  It will silently ignore calls to add if it is at maximum capacity.
 */
template<typename Type>
class Bag
{
public:
	Bag(int maxsize)
	: elements(new Type[maxsize]), indexes(new int[maxsize]), mCapacity(maxsize), mSize(Innocent
	{
		for (int i = 0; i < maxsize; ++i) {
			indexes[i] = i;
		}
	}

	~Bag()
	{
		delete[] elements;
		delete[] indexes;
	}

	void add(const Type& val) {
		if (!isFull())
		{
			elements[indexes[mSize++]] = val;
		}
	}

	void remove(int index) {
		int tmp = indexes[--mSize];
		indexes[mSize] = indexes
; indexes
= tmp; } Type& operator[](int index) { return elements[indexes
]; } int size() { return mSize; } int capacity() { return mCapacity; } bool isFull() { return (mSize == mCapacity); } void callAll(void (Type::*f)()) { for (int i=0; i < mSize; ++i) { (elements[indexes[i]].*f)(); } } // Calls a single arity member function on all elements template<typename A> void callAll1(void (Type::*f)(A), A arg1) { for (int i=0; i < mSize; ++i) { (elements[indexes[i]].*f)(arg1); } } void removeIf(bool (Type::*pred)()) { int i=0; while(i < mSize) { if((elements[indexes[i++]].*pred)()) { remove(--i); } } } private: Type* elements; int* indexes; int mCapacity; int mSize; }; class MyWidget : public Widget, public TimerListener, PointerListener { public: MyWidget(int x=0, int y=0, int width=0, int height=0, Widget* parent=NULL) : Widget(x, y, width, height, parent), spriteMap(STAR_SMAP, 8, 3), particleBag(100), startMs(maGetMilliSecondCount()), pointerIsDown(false), pointerListenerToken(this), timerListenerToken(this, 20) // 50 Hz { this->setBackgroundColor(0x000000); } ~MyWidget() { } void drawWidget() { particleBag.callAll(&Particle::draw); } void tick(int dt) { time += dt; if (pointerIsDown) { float dt_s = (float)dt / 1000.0; float mystery_factor = 0.5; // This should be 1.0 in my mind. Don't understand. float pointerVelX = mystery_factor * (newPointerPos.x - lastPointerPos.x) / dt_s; float pointerVelY = mystery_factor * (newPointerPos.y - lastPointerPos.y) / dt_s; lastPointerPos = newPointerPos; addParticle(lastPointerPos, pointerVelX, pointerVelY); } particleBag.callAll1(&Particle::tick, dt); particleBag.removeIf(&Particle::dead); } void runTimerEvent() { int now = maGetMilliSecondCount(); int dt = now - startMs; startMs = now; tick(dt); requestRepaint(); } void putRandomCircleCoordinates(int r, int& x, int& y) { int r2 = r*r; do { x = rand() % (2*r) - r; y = rand() % (2*r) - r; } while ((x*x + y*y) >= r2); } void addParticle(MAPoint2d p, float pointerVelX, float pointerVelY) { int dx; int dy; int dvx; int dvy; putRandomCircleCoordinates(20, dx, dy); // Spread out the origin putRandomCircleCoordinates(20, dvx, dvy); // Disturb the initial velocity int rotation_ms = ((rand() & 2)-1)*(rand() % 400 + 100); // -0.5 to -0.1 and 0.1 to 0.5 Particle particle(spriteMap, p.x + dx, p.y + dy, pointerVelX + dvx, pointerVelY + dvy, rotation_ms, rand() % 750 + 250, // Sparkle interval between 0.25 and 1 secs 0.2, // Not shown (because of sparkle) 20% of the time rand() % 2, // Row of graphics in sprite map rand() % 4000 + 1000); if (!particleBag.isFull()) { particleBag.add(particle); } else { // Replace random sprite particleBag[rand() % particleBag.capacity()] = particle; } } void pointerPressEvent(MAPoint2d p) { pointerIsDown = true; MAUtil::Point rp(p.x - bounds.x, p.y - bounds.y); lastPointerPos = rp; newPointerPos = rp; } void pointerMoveEvent(MAPoint2d p) { MAUtil::Point rp(p.x - bounds.x, p.y - bounds.y); newPointerPos = rp; }; void pointerReleaseEvent(MAPoint2d p) { pointerIsDown = false; }; private: SpriteMap spriteMap; Bag<Particle> particleBag; int startMs; bool pointerIsDown; MAPoint2d newPointerPos; MAPoint2d lastPointerPos; int time; PointerListenerToken pointerListenerToken; TimerListenerToken timerListenerToken; }; class MyScreen : public Screen { public: MyScreen() //: mainSpace(0, 0, 0, 0, NULL), myWidget(50, 50, 100, 100, &mainSpace) { setMain(&myWidget); //setMain(&mainSpace); } ~MyScreen() { } private: //Label mainSpace; MyWidget myWidget; }; class ParticleMoblet : public Moblet { public: ParticleMoblet() { screen.show(); } ~ParticleMoblet() { } private: MyScreen screen; }; extern "C" int MAMain() { ParticleMoblet moblet; Moblet::run(&moblet); return 0; };
Anthony Hartley
Hartley's picture
Offline
Mobile Wizard
Joined: 25 Sep 2007
Posts:

Very cool offe.

You could add 2d rotation.

Would you like an example page on the site ?

Best regards

Tony Hartley

offe
offe's picture
Offline
Joined: 7 Jan 2010
Posts:

I'll look into 2d rotation. It might be nice.

What do you mean by "Would you like an example page on the site?".

If you wonder if I think there should be a page with some sample code on the page, sure. All programmers prefer code to documentation at first.

Alex Jonsson
alexj's picture
Offline
Mobile Conjurer
Joined: 8 Sep 2009
Posts:

So true,

I'll ask Tony where best put contrib examples.
Good Job in any case, very neat thingy. You know it has a cousin here at MoSync, that you might find something useful in:
http://www.mosync.com/content/advgraphics/

best regards
Alex