Введение

В этот раз речь пойдет о разработке мобильных приложений, а если быть точным, то мобильных игр, с библиотекой wxWidgets (порт wxWinCE).
О том, как собрать wxWidgets для разработки приложений для Windows Mobile я уже писал ранее здесь. Как создать простейшее приложение с wxWinCE, рассказано в этой статье.
Здесь и далее по тексту подразумевается, что читатель уже может самостоятельно создать простейшее приложение с wxWinCE, а также настроить параметры сборки для PocketPC и Smartphone.

Каркас приложения

Так случилось, что я решил попробовать себя в написании мобильных игр. После небольшого исследования пришел к выводу, что в простейшем случае для этой задачи вполне может подойти архитектура Документ/Представление (Document/View).

О создании приложений, использующих архитектуру Документ/Представление на wxWidgets, я писал ранее (первая, вторая, третья часть). Т.е. для начала нам необходимо получить: приложение с графическим интерфейсом, главная форма которого содержит канву, на которой, собственно, происходит отрисовка сцены. Кажданя новая игра представляет собой пару документ/представление. Класс документа содержит информацию о текущем состоянии игры, класс представления обеспечивает игровую логику. Канва кроме, непосредственно, отображения сцены обеспечивает поддержку double-buffering’а.
HabraSnakeDocument.h

#ifndef _HABRA_SNAKE_DOCUMENT_H
#define _HABRA_SNAKE_DOCUMENT_H

#include <wx/wx.h>
#include <wx/docview.h>

class HabraSnakeDocument : public wxDocument
{
	DECLARE_DYNAMIC_CLASS(HabraSnakeDocument)
public:
	HabraSnakeDocument();
};

#endif

HabraSnakeDocument.cpp

#include "HabraSnakeDocument.h"

IMPLEMENT_DYNAMIC_CLASS(HabraSnakeDocument, wxDocument)

HabraSnakeDocument::HabraSnakeDocument()
{
}

HabraSnakeView.h

#ifndef _HABRA_SNAKE_VIEW_H
#define _HABRA_SNAKE_VIEW_H

#include <wx/wx.h>
#include <wx/docview.h>

class HabraSnakeView : public wxView
{
	DECLARE_DYNAMIC_CLASS(HabraSnakeView)
public:
	HabraSnakeView();
	virtual void OnDraw(wxDC* dc);	
	virtual void OnUpdate(wxView *sender, wxObject *hint = (wxObject *) NULL);
	virtual bool OnClose(bool deleteWindow = true);
};

#endif

HabraSnakeView.cpp

#include "HabraSnakeView.h"
#include "HabraSnakeMainFrame.h"
#include "HabraSnakeCanvas.h"

IMPLEMENT_DYNAMIC_CLASS(HabraSnakeView, wxView)

HabraSnakeView::HabraSnakeView()
{
	do 
	{
		HabraSnakeMainFrame * mainFrame = 
			wxDynamicCast(wxTheApp->GetTopWindow(), HabraSnakeMainFrame);
		if(!mainFrame) break;
		HabraSnakeCanvas * canvas = mainFrame->m_Canvas;
		SetFrame(canvas);
		canvas->SetView(this);
		canvas->Refresh();
	} 
	while (false);
}

void HabraSnakeView::OnDraw(wxDC* dc)
{
}

void HabraSnakeView::OnUpdate(wxView *sender, wxObject *hint)
{
	GetFrame()->Refresh();
}

bool HabraSnakeView::OnClose(bool deleteWindow)
{
	if (!GetDocument()->Close())
	{
		return false;
	}
	HabraSnakeCanvas * frame = wxDynamicCast(GetFrame(), HabraSnakeCanvas);
	if(frame)
	{
		frame->SetView(NULL);
		frame->Refresh();
	}
	SetFrame(NULL);
	Activate(false);
	return true;
}

HabraSnakeMainFrame.h

#ifndef _HABRASNAKEMAINFRAME_H_
#define _HABRASNAKEMAINFRAME_H_

#include "wx/docview.h"

class HabraSnakeCanvas;

#define ID_HABRASNAKEMAINFRAME 10000
#define ID_FOREIGN 10007
#define SYMBOL_HABRASNAKEMAINFRAME_STYLE wxCAPTION|wxRESIZE_BORDER|wxSYSTEM_MENU|wxCLOSE_BOX
#define SYMBOL_HABRASNAKEMAINFRAME_TITLE _("HabraSnake")
#define SYMBOL_HABRASNAKEMAINFRAME_IDNAME ID_HABRASNAKEMAINFRAME
#define SYMBOL_HABRASNAKEMAINFRAME_SIZE wxSize(400, 300)
#define SYMBOL_HABRASNAKEMAINFRAME_POSITION wxDefaultPosition

class HabraSnakeMainFrame: public wxDocParentFrame
{    
    DECLARE_CLASS( HabraSnakeMainFrame )
    DECLARE_EVENT_TABLE()

public:
    HabraSnakeMainFrame( wxDocManager *manager, wxFrame *parent, 
        wxWindowID id = SYMBOL_HABRASNAKEMAINFRAME_IDNAME, 
        const wxString& caption = SYMBOL_HABRASNAKEMAINFRAME_TITLE, 
        const wxPoint& pos = SYMBOL_HABRASNAKEMAINFRAME_POSITION, 
        const wxSize& size = SYMBOL_HABRASNAKEMAINFRAME_SIZE, 
        long style = SYMBOL_HABRASNAKEMAINFRAME_STYLE );
    bool Create( wxDocManager *manager, wxFrame *parent, 
        wxWindowID id = SYMBOL_HABRASNAKEMAINFRAME_IDNAME, 
        const wxString& caption = SYMBOL_HABRASNAKEMAINFRAME_TITLE, 
        const wxPoint& pos = SYMBOL_HABRASNAKEMAINFRAME_POSITION, 
        const wxSize& size = SYMBOL_HABRASNAKEMAINFRAME_SIZE, 
        long style = SYMBOL_HABRASNAKEMAINFRAME_STYLE );
    ~HabraSnakeMainFrame();
    void Init();
    void CreateControls();

    void OnABOUTClick( wxCommandEvent& event );
    void OnEXITClick( wxCommandEvent& event );
    wxBitmap GetBitmapResource( const wxString& name );
    wxIcon GetIconResource( const wxString& name );
    HabraSnakeCanvas* m_Canvas;
};

#endif

HabraSnakeMainFrame.cpp

#include "wx/wxprec.h"

#ifdef __BORLANDC__
#pragma hdrstop
#endif

#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif

#include "HabraSnakeMainFrame.h"
#include "HabraSnakeCanvas.h"
// #include "AboutDialog.h"

IMPLEMENT_CLASS( HabraSnakeMainFrame, wxDocParentFrame )

BEGIN_EVENT_TABLE( HabraSnakeMainFrame, wxDocParentFrame )
    EVT_MENU( wxID_ABOUT, HabraSnakeMainFrame::OnABOUTClick )
    EVT_MENU( wxID_EXIT, HabraSnakeMainFrame::OnEXITClick )
END_EVENT_TABLE()

HabraSnakeMainFrame::HabraSnakeMainFrame( wxDocManager *manager, 
        wxFrame *parent, wxWindowID id, const wxString& caption, 
        const wxPoint& pos, const wxSize& size, long style )
    : wxDocParentFrame( manager, parent, id, caption, pos, size, style )
{
    Init();
    Create( manager, parent, id, caption, pos, size, style );
}

bool HabraSnakeMainFrame::Create( wxDocManager *manager, wxFrame *parent, 
        wxWindowID id, const wxString& caption, const wxPoint& pos, 
        const wxSize& size, long style )
{
    SetParent(parent);
    CreateControls();
    Centre();
    return true;
}

HabraSnakeMainFrame::~HabraSnakeMainFrame()
{
}

void HabraSnakeMainFrame::Init()
{
    m_Canvas = NULL;
}

void HabraSnakeMainFrame::CreateControls()
{
    HabraSnakeMainFrame* itemDocParentFrame1 = this;

    wxMenuBar* menuBar = new wxMenuBar;
    wxMenu* itemMenu3 = new wxMenu;
    itemMenu3->Append(wxID_NEW, _("New\tCtrl+N"), _T(""), wxITEM_NORMAL);
    itemMenu3->AppendSeparator();
    itemMenu3->Append(wxID_ABOUT, _("About..."), _T(""), wxITEM_NORMAL);
    itemMenu3->AppendSeparator();
    itemMenu3->Append(wxID_EXIT, _("Exit\tAlt+F4"), _T(""), wxITEM_NORMAL);
    menuBar->Append(itemMenu3, _("File"));
    itemDocParentFrame1->SetMenuBar(menuBar);

    m_Canvas = new HabraSnakeCanvas( itemDocParentFrame1, ID_FOREIGN, 
        wxDefaultPosition, wxSize(100, 100), wxNO_BORDER );

#if defined(__WXMSW__)
	SetIcon(wxIcon(wxT("wxICON_AAA")));
#endif
#if !defined(__WXWINCE__)
	SetSize(350, 450);
	SetMinSize(GetSize());
#endif
	m_Canvas->RefreshScene();
}

wxBitmap HabraSnakeMainFrame::GetBitmapResource( const wxString& name )
{
    wxUnusedVar(name);
    return wxNullBitmap;
}

wxIcon HabraSnakeMainFrame::GetIconResource( const wxString& name )
{
    wxUnusedVar(name);
    return wxNullIcon;
}

void HabraSnakeMainFrame::OnABOUTClick( wxCommandEvent& event )
{
    // AboutDialog * dlg = new AboutDialog(this);
    // dlg->ShowModal();
    // dlg->Destroy();
}

void HabraSnakeMainFrame::OnEXITClick( wxCommandEvent& event )
{
    Close();
}

HabraSnakeApp.h

#ifndef _HABRASNAKEAPP_H_
#define _HABRASNAKEAPP_H_

#include "wx/image.h"
#include "HabraSnakeMainFrame.h"

class wxDocManager;

class HabraSnakeApp: public wxApp
{    
    DECLARE_CLASS( HabraSnakeApp )
    DECLARE_EVENT_TABLE()
	wxDocManager * m_DocManager;
public:
    HabraSnakeApp();
    void Init();
    virtual bool OnInit();
    virtual int OnExit();
};

DECLARE_APP(HabraSnakeApp)

#endif

HabraSnakeApp.cpp

#include "wx/wxprec.h"

#ifdef __BORLANDC__
#pragma hdrstop
#endif

#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif

#include "HabraSnakeApp.h"
#include "HabraSnakeDocument.h"
#include "HabraSnakeView.h"

IMPLEMENT_APP( HabraSnakeApp )

IMPLEMENT_CLASS( HabraSnakeApp, wxApp )

BEGIN_EVENT_TABLE( HabraSnakeApp, wxApp )
END_EVENT_TABLE()

HabraSnakeApp::HabraSnakeApp()
{
    Init();
}

void HabraSnakeApp::Init()
{
	m_DocManager = new wxDocManager;
	m_DocManager->SetMaxDocsOpen(1);
	new wxDocTemplate(m_DocManager, _("HabraSnake Game"), 
		wxEmptyString, wxEmptyString,
		wxEmptyString, wxT("HabraSnakeDoc"), wxT("HabraSnakeView"), 
		CLASSINFO(HabraSnakeDocument), CLASSINFO(HabraSnakeView));
}

bool HabraSnakeApp::OnInit()
{
#if wxUSE_LIBJPEG
	wxImage::AddHandler(new wxJPEGHandler);
#endif
#if wxUSE_XPM
	wxImage::AddHandler(new wxXPMHandler);
#endif
	HabraSnakeMainFrame* mainWindow = 
		new HabraSnakeMainFrame( m_DocManager,  NULL);
	SetTopWindow(mainWindow);
	mainWindow->Show(true);
    return true;
}

int HabraSnakeApp::OnExit()
{
	wxDELETE(m_DocManager);
	return wxApp::OnExit();
}

Такое большое количество кода в классе главной формы и в классе приложения не должно пугать. 90% этого кода сгенерировано редактором DialogBlocks. Руками пришлось дописать всего несколько строк. В классе главной формы это следующие строки в методе CreateControls():

HabraSnakeMainFrame.cpp

#if defined(__WXMSW__)
	SetIcon(wxIcon(wxT("wxICON_AAA")));
#endif
#if !defined(__WXWINCE__)
	SetSize(350, 450);
	SetMinSize(GetSize());
#endif
	m_Canvas->RefreshScene();
	wxCommandEvent event(wxEVT_COMMAND_MENU_SELECTED, wxID_NEW);
	ProcessEvent(event);
	m_Canvas->SetFocus();
}

В классе приложения это код методов Init() и OnExit().
Кроме, собственно, исходного кода класса главной формы и класса приложения, редактор DialogBlocks генерирует файл ресурсов, который обязательно необходимо добавить в проект. Без него приложение для Windows Mobile не будет работать корректно. Иконка wxICON_AAA описана в указанном выше файле ресурсов.

Немного больше внимания необходимо уделить компоненту, на котором будет происходить отрисовка сцены:
HabraSnakeCanvas.h

#ifndef _HABRASNAKECANVAS_H_
#define _HABRASNAKECANVAS_H_

#include <wx/docview.h>

class HabraSnakeCanvas;

#define ID_HABRASNAKECANVAS 10001
#define SYMBOL_HABRASNAKECANVAS_STYLE wxSIMPLE_BORDER
#define SYMBOL_HABRASNAKECANVAS_IDNAME ID_HABRASNAKECANVAS
#define SYMBOL_HABRASNAKECANVAS_SIZE wxSize(100, 100)
#define SYMBOL_HABRASNAKECANVAS_POSITION wxDefaultPosition

class HabraSnakeCanvas: public wxWindow
{    
    DECLARE_DYNAMIC_CLASS( HabraSnakeCanvas )
    DECLARE_EVENT_TABLE()
public:
    HabraSnakeCanvas();
    HabraSnakeCanvas(wxWindow* parent, wxWindowID id = ID_HABRASNAKECANVAS, 
	const wxPoint& pos = wxDefaultPosition, 
	const wxSize& size = wxSize(100, 100), long style = wxSIMPLE_BORDER);
    bool Create(wxWindow* parent, wxWindowID id = ID_HABRASNAKECANVAS, 
	const wxPoint& pos = wxDefaultPosition, 
	const wxSize& size = wxSize(100, 100), long style = wxSIMPLE_BORDER);
    ~HabraSnakeCanvas();
    void Init();
    void CreateControls();

    void OnSize( wxSizeEvent& event );
    void OnPaint( wxPaintEvent& event );
    void OnEraseBackground( wxEraseEvent& event );
    void OnLeftDown( wxMouseEvent& event );

    wxView * GetView() const { return m_View ; }
    void SetView(wxView * value) { m_View = value ; }

    wxBitmap GetBitmapResource( const wxString& name );
    wxIcon GetIconResource( const wxString& name );
	void DrawBackground( wxDC &dc );
	void RefreshScene();
private:
	wxView * m_View;
	wxBitmap m_BackgroundBitmap;
	wxBitmap m_DoubleBufferBitmap;
	wxMemoryDC m_DoubleBufferDC;
};

#endif

HanraSnakeCanvas.cpp

#include "wx/wxprec.h"

#ifdef __BORLANDC__
#pragma hdrstop
#endif

#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif

#include "HabraSnakeCanvas.h"
#include <wx/mstream.h>

#include "background_jpg.h"

IMPLEMENT_DYNAMIC_CLASS( HabraSnakeCanvas, wxWindow )

BEGIN_EVENT_TABLE( HabraSnakeCanvas, wxWindow )
    EVT_SIZE( HabraSnakeCanvas::OnSize )
    EVT_PAINT( HabraSnakeCanvas::OnPaint )
    EVT_ERASE_BACKGROUND( HabraSnakeCanvas::OnEraseBackground )
    EVT_LEFT_DOWN( HabraSnakeCanvas::OnLeftDown )
END_EVENT_TABLE()

HabraSnakeCanvas::HabraSnakeCanvas()
{
    Init();
}

HabraSnakeCanvas::HabraSnakeCanvas(wxWindow* parent, wxWindowID id, 
	const wxPoint& pos, const wxSize& size, long style)
{
    Init();
    Create(parent, id, pos, size, style);
}

bool HabraSnakeCanvas::Create(wxWindow* parent, wxWindowID id, 
	const wxPoint& pos, const wxSize& size, long style)
{
    wxWindow::Create(parent, id, pos, size, style);
    CreateControls();
    return true;
}

HabraSnakeCanvas::~HabraSnakeCanvas()
{
}

void HabraSnakeCanvas::Init()
{
    m_View = NULL;
}

void HabraSnakeCanvas::CreateControls()
{    
	wxMemoryInputStream in(background_jpg, sizeof(background_jpg));
	m_BackgroundBitmap = wxBitmap(wxImage(in));
#if defined(__WXWINCE__)
	wxFont font = GetFont();
	font.SetPointSize(8);
	SetFont(font);
#endif
}

void HabraSnakeCanvas::OnPaint( wxPaintEvent& event )
{
    wxPaintDC dc(this);
	if(m_DoubleBufferDC.IsOk())
	{
		dc.Blit(0, 0, m_DoubleBufferDC.GetSize().GetWidth(), 
			m_DoubleBufferDC.GetSize().GetHeight(), &m_DoubleBufferDC, 0, 0);
	}
	else
	{
		dc.SetBackground(wxBrush(GetBackgroundColour()));
		dc.Clear();
	}
}

void HabraSnakeCanvas::DrawBackground( wxDC &dc )
{
	if(m_BackgroundBitmap.IsOk())
	{
		int w(0), h(0);
		dc.GetSize(&w, &h);
		for(int x = 0; x < w; x += m_BackgroundBitmap.GetWidth())
		{
			for(int y = 0; y < h; y += m_BackgroundBitmap.GetHeight())
			{
				dc.DrawBitmap(m_BackgroundBitmap, x, y);
			}
		}
	}
}

void HabraSnakeCanvas::OnEraseBackground( wxEraseEvent& event )
{
}

wxBitmap HabraSnakeCanvas::GetBitmapResource( const wxString& name )
{
    wxUnusedVar(name);
    return wxNullBitmap;
}

wxIcon HabraSnakeCanvas::GetIconResource( const wxString& name )
{
    wxUnusedVar(name);
    return wxNullIcon;
}

void HabraSnakeCanvas::OnLeftDown( wxMouseEvent& event )
{
	SetFocus();
}

void HabraSnakeCanvas::RefreshScene()
{
	if(m_View)
	{
		DrawBackground(m_DoubleBufferDC);
		m_DoubleBufferDC.SetFont(GetFont());
		m_DoubleBufferDC.SetBrush(wxBrush(GetBackgroundColour()));
		m_DoubleBufferDC.SetPen(*wxBLACK_PEN);
		m_View->OnDraw(&m_DoubleBufferDC);
	}
}

void HabraSnakeCanvas::OnSize( wxSizeEvent& event )
{
	m_DoubleBufferDC.SelectObject(wxNullBitmap);
	m_DoubleBufferBitmap = wxBitmap(event.GetSize().GetWidth(), 
		event.GetSize().GetHeight());
	m_DoubleBufferDC.SelectObject(m_DoubleBufferBitmap);
	RefreshScene();
	Refresh();
}

Рассмотрим базовый функционал компонента:

  • В компоненте вручную реализован double-buffering. Такой подход обусловлен тем, что при использовании класса wxPaintDC для отрисовки сцены будет заметно мерцание. Использование wxBufferedPaintDC не очень приемлемо для мобильных устройств т.к. при каждом вызове обработчика события wxEVT_PAINT будет заново создаваться объект wxBufferedPaintDC, а он, в свою очередь, каждый раз будет заново создавать изображение в памяти, размером равным размеру компонента. Это очень негативно сказывается на ресурсоемкости приложения. Более подробную информацию о реализации double-buffering’а в wxWidgets вручную можно почитать здесь.
  • При изменении размеров компонента (метод OnSize()) заново пересоздается изображение для double-buffering’а и вызывается метод RefreshScene().
  • Метод RefreshScene() отрисовывает фоновое изображение и вызывает метод OnDraw() класса представления.
  • Класс компонента не привязан к какому-то особому классу представления и может быть использован для работы с различными типами представлений (это значит, что с таким подходом этот компонент можно использовать для отображения сцен из различных игр).
  • Обработчик события wxEVT_KEY_DOWN (нажатия клавиш) передает объект wxKeyEvent классу представления. Это значит, что обработкой нажатия клавиш должен заниматься класс представления.
  • Обработчик события нажатия левой кнопки мыши (метод OnLeftDown()) устанавливает фокус на наш компонент (это необходимо для того чтобы компонент смог получать события нажатия клавиш, т.к. только окно, имеющее фокус ввода может получать сообщения о нажатии клавиш).
  • Пустой обработчик события wxEVT_ERASE_BACKGROUND нам необходим для того чтобы избежать мерцания.
  • В обработчике события wxEVT_PAINT происходит копирование изображения из double-buffer’а на wxPaintDC.
  • Таким образом, мы получили универсальный класс канвы, который может использоваться для отрисовки сцен различных игр с подобной архитектурой приложения.

В приведенном выше коде есть ссылка на файл background_jpg.h. Это файл, содержащий jpeg-изображение, преобразованное в c-style массив байт с помощью утилиты Bin2C:
background_jpg.h

#ifndef _BACKGROUND_JPG_H
#define _BACKGROUND_JPG_H

unsigned char background_jpg[] ={
	0xFF,0xD8,0xFF,0xE0,0x00,0x10,0x4A,0x46,0x49,0x46,0x00,0x01,0x01,0x00,
	...
	0x94,0xFF,0xD9
};

#endif

Добавим немного логики

Пора добавить логику в наше приложение. Начнем с изменения класса документа. Класс документа у нас должен хранить состояние игры (текущее положение змейки, положение “цели”, признак окончания игры, количество набранных очков).
HabraSnakeDocument.h

#ifndef _HABRA_SNAKE_DOCUMENT_H
#define _HABRA_SNAKE_DOCUMENT_H

#include <wx/wx.h>
#include <wx/docview.h>
#include <wx/list.h>

/// Список точек
WX_DECLARE_LIST(wxPoint, wxPointList);

class HabraSnakeDocument : public wxDocument
{
	/// Размер игрового поля
	static wxSize GameFieldSize;
	DECLARE_DYNAMIC_CLASS(HabraSnakeDocument)
	/// "Сегменты" змейки
	wxPointList m_Snake;
	/// Координаты "цели" для змейки
	wxPoint m_Target;
	/// Признак окончания игры
	bool m_GameOver;
	/// Количество очков
	unsigned int m_Score;
	/// Инициализировать змейку
	void InitSnake();
	/// Пересоздать "цель"
	void CreateNewTarget();
	void ProcessNewPoint(wxPoint * newPoint, bool & targetFound);
	/// Проверка, можно ли переместиться в указанную точку
	bool CanMoveTo(wxPoint & pos);
	/// Проверка, не умерла ли змейка 
	/// Если змейка зохавала кусок себя, то все, каюк
	bool IsSnakeDead();
public:
	HabraSnakeDocument();
	/// Возвращает список сегментов змейки
	const wxPointList & GetSnake() const;
	/// Возвращает координаты "цели"
	const wxPoint & GetTarget() const;
	/// Выполнить шаг влево
	bool MoveLeft(bool & targetFound);
	/// Выполнить шаг вверх
	bool MoveUp(bool & targetFound);
	/// Выполнить шаг вправо
	bool MoveRight(bool & targetFound);
	/// Выполнить шаг вниз
	bool MoveDown(bool & targetFound);
	/// Возвращает признак завершения игры
	bool IsGameOver();
	/// Возвращает количество очков
	unsigned int GetScore();
	/// Возвращает размер игрового поля
	static wxSize GetGameFieldSize();
};

#endif

HabraSnakeDocument.cpp

#include "HabraSnakeDocument.h"
#include <wx/listimpl.cpp>

WX_DEFINE_LIST(wxPointList)

IMPLEMENT_DYNAMIC_CLASS(HabraSnakeDocument, wxDocument)

wxSize HabraSnakeDocument::GameFieldSize = wxSize(25, 50);

HabraSnakeDocument::HabraSnakeDocument()
{
	srand(time(NULL));
	m_GameOver = false;
	m_Score = 0;
	CreateNewTarget();
	InitSnake();
}

void HabraSnakeDocument::InitSnake()
{
	m_Snake.DeleteContents(true);
	m_Snake.Append(new wxPoint(15,25));
	m_Snake.Append(new wxPoint(16,25));
	m_Snake.Append(new wxPoint(17,25));
}

wxSize HabraSnakeDocument::GetGameFieldSize()
{
	return HabraSnakeDocument::GameFieldSize;
}

const wxPointList & HabraSnakeDocument::GetSnake() const
{
	return m_Snake;
}

bool HabraSnakeDocument::IsGameOver()
{
	return m_GameOver;
}

unsigned int HabraSnakeDocument::GetScore()
{
	return m_Score;
}

const wxPoint & HabraSnakeDocument::GetTarget() const
{
	return m_Target;
}

void HabraSnakeDocument::CreateNewTarget()
{
	m_Target.x = rand() % GameFieldSize.GetWidth();
	m_Target.y = rand() % GameFieldSize.GetHeight();
}

bool HabraSnakeDocument::IsSnakeDead()
{
	do 
	{
		wxPointList::Node * firstNode = m_Snake.GetFirst();
		if(!firstNode) break;
		wxPoint * head = firstNode->GetData();
		if(!head) break;
		for(wxPointList::Node * node = firstNode->GetNext(); 
			node; node = node->GetNext())
		{
			wxPoint * point = node->GetData();
			if(*head == *point) return true;
		}
	} 
	while (false);
	return false;
}

bool HabraSnakeDocument::CanMoveTo(wxPoint & pos)
{
	do 
	{
		wxPointList::Node * second = m_Snake.Item(1);
		if(!second) break;
		wxPoint * secondPoint = second->GetData();
		if(!secondPoint) break;
		if((secondPoint->x != pos.x) || (secondPoint->y != pos.y)) break;
		return false;
	} 
	while (false);
	return true;
}

Итак, что мы тут добавили:

  • Поле m_Snake содержит список координат сегментов змейки.
  • Поле m_Target содержит координаты “цели” для змейки (цель – это та штука, которую змейка должна “съесть” чтобы вырасти).
  • Поле m_GameOver – это признак завершения игры. Содержит true, если игра закончена.
  • Поле m_Score содержит количество набранных очков.
  • Метод InitSnake() инициализирует список сегментов змейки начальными значениями (изначально в змейке 3 сегмента).
  • Метод CreateNewTarget() устанавливает случайные значения координат «цели» в пределах игрового поля.
  • Метод IsSnakeDead() выполняет проверку положения первого сегмента змейки. Если координаты первого сегмента равны координатам любого другого сегмента, метод возвращает true, в противном случае возвращает false.
  • Метод CanMoveTo() выполняет проверку возможности перемещения змейки в указанную ячейку на игровом поле. Перемещение возможно только «вперед», т.е. указанная ячейка должна быть по направлению движения змейки. Если по указанным координатам находится второй сегмент змейки, то в эту ячейку перемещение невозможно и метод возвращает false.

Далее нам необходимо научить змейку перемещаться. Для этого у нас есть методы MoveLeft(), MoveUp(), MoveRight(), MoveDown().
HabraSnakeDocument.cpp

bool HabraSnakeDocument::MoveLeft(bool & targetFound)
{
	do
	{
		if(m_GameOver) break;
		wxPointList::Node * node = m_Snake.GetFirst();
		if(!node) break;
		wxPoint * point = node->GetData();
		if(!point) break;
		wxPoint * newPoint = new wxPoint(
			(point->x-1 < 0) ? HabraSnakeDocument::GameFieldSize.GetWidth()-1 :
			 point->x-1, point->y);
		if(!CanMoveTo(*newPoint))
		{
			wxDELETE(newPoint);
			break;
		}
		ProcessNewPoint(newPoint, targetFound);
		if(IsSnakeDead()) m_GameOver = true;
		return true;
	}
	while(false);
	return false;
}

bool HabraSnakeDocument::MoveUp(bool & targetFound)
{
	do
	{
		if(m_GameOver) break;
		wxPointList::Node * node = m_Snake.GetFirst();
		if(!node) break;
		wxPoint * point = node->GetData();
		if(!point) break;
		wxPoint * newPoint = new wxPoint(
			point->x,
			(point->y-1 < 0) ? HabraSnakeDocument::GameFieldSize.GetHeight()-1 : 
			point->y-1);
		if(!CanMoveTo(*newPoint))
		{
			wxDELETE(newPoint);
			break;
		}
		ProcessNewPoint(newPoint, targetFound);
		if(IsSnakeDead()) m_GameOver = true;
		return true;
	}
	while(false);
	return false;
}

bool HabraSnakeDocument::MoveRight(bool & targetFound)
{
	do
	{
		if(m_GameOver) break;
		wxPointList::Node * node = m_Snake.GetFirst();
		if(!node) break;
		wxPoint * point = node->GetData();
		if(!point) break;
		wxPoint * newPoint = new wxPoint(
			(point->x+1 > HabraSnakeDocument::GameFieldSize.GetWidth()-1) ? 0 : 
			point->x+1, point->y);
		if(!CanMoveTo(*newPoint))
		{
			wxDELETE(newPoint);
			break;
		}
		ProcessNewPoint(newPoint, targetFound);
		if(IsSnakeDead()) m_GameOver = true;
		return true;
	}
	while(false);
	return false;
}

bool HabraSnakeDocument::MoveDown(bool & targetFound)
{
	do
	{
		if(m_GameOver) break;
		wxPointList::Node * node = m_Snake.GetFirst();
		if(!node) break;
		wxPoint * point = node->GetData();
		if(!point) break;
		wxPoint * newPoint = new wxPoint(
			point->x,
			(point->y+1 > HabraSnakeDocument::GameFieldSize.GetHeight()-1) ? 0 : 
			point->y+1);
		if(!CanMoveTo(*newPoint))
		{
			wxDELETE(newPoint);
			break;
		}
		ProcessNewPoint(newPoint, targetFound);
		if(IsSnakeDead()) m_GameOver = true;
		return true;
	}
	while(false);
	return false;
}

Каждый из приведенных выше четырех методов проверяет возможность перемещения змейки в новую ячейку и, если перемещение возможно, вызывает метод ProcessNewPoint(), код которого приведен ниже:
HabraSnakeDocument.cpp

void HabraSnakeDocument::ProcessNewPoint(wxPoint * newPoint, bool & targetFound)
{
	if(*newPoint == m_Target)
	{
		m_Score += 10;
		targetFound = true;
		CreateNewTarget();
	}
	else
	{
		targetFound = false;
		m_Snake.DeleteNode(m_Snake.GetLast());
	}
	m_Snake.Insert((size_t)0, newPoint);
}

Метод ProcessNewPoint() проверяет равенство координат новой ячейки координатам “цели” и затем, если координаты новой ячейки и координаты цели совпадают, то просто добавляет эту ячейку в начало списка сегментов змейки. “цель” в этом случае перемещается в новую ячейку со случайно выбранными координатами (как уже говорилось ранее, для этого используется метод CreateNewTarget()). Если же координаты не совпадают, то последний сегмент змейки удаляется.

Отрисовка сцены

Работу над классом документа, который содержит состояние игры, мы закончили, теперь нам необходимо организовать визуальное представление состояния игры, т.е. отрисовку сцены.
Эту задачу у нас будет выполнять класс представления:
HabraSnakeView.h

class HabraSnakeView : public wxView
{
	wxRect GetGameFieldRectangle(const wxSize & size, int rectSize);
	void DrawGrid( wxDC* dc, wxRect &rect, int rectSize );
	void DrawSnake(wxDC * dc, wxRect & rect, int rectSize, const wxPointList & snake);
	void DrawTarget(wxDC * dc, wxRect & rect, int rectSize, const wxPoint & target);
	void DrawGameOverLabel(wxDC * dc, const wxRect & rect);
	void DrawScore(wxDC * dc, unsigned int score);
	void RefreshScene();
public:
	...
	virtual void OnDraw(wxDC* dc);	
	...
}; 

HabraSnakeView.cpp

void HabraSnakeView::OnDraw(wxDC* dc)
{
	int rectSize = dc->GetSize().GetWidth()/
            HabraSnakeDocument::GetGameFieldSize().GetWidth();
	rectSize = wxMin(rectSize, 
		(int)dc->GetSize().GetHeight()/
                HabraSnakeDocument::GetGameFieldSize().GetHeight());
	wxRect rect = GetGameFieldRectangle(dc->GetSize(), rectSize);
	DrawGrid(dc, rect, rectSize);
	do 
	{
		HabraSnakeDocument * document = wxDynamicCast(GetDocument(), HabraSnakeDocument);
		if(!document) break;
		DrawScore(dc, document->GetScore());
		if(!document->IsGameOver())
		{
			DrawSnake(dc, rect, rectSize, document->GetSnake());
			DrawTarget(dc, rect, rectSize, document->GetTarget());
		}
		else
		{
			DrawGameOverLabel(dc, rect);
		}
	} 
	while (false);
}

void HabraSnakeView::DrawScore(wxDC * dc, unsigned int score)
{
	int w(0), h(0);
	dc->GetSize(&w, &h);
	wxString scoreLabel = _("SCORE");
	wxString scoreValue = wxString::Format(wxT("%u"), score);
	wxSize textSize = dc->GetTextExtent(scoreLabel);
	wxRect scoreRect(wxPoint(0,0), textSize);
	textSize = dc->GetTextExtent(scoreValue);
	scoreRect.SetWidth(wxMax(scoreRect.GetWidth(), textSize.GetWidth()));
	scoreRect.SetHeight((int)(scoreRect.GetHeight() + (double)6 / (double)5 * 
		(double)textSize.GetHeight()));
	scoreRect.Inflate(5, 5);
#if defined(__WXWINCE__)
	int distance = 5;
#else
	int distance = 10;
#endif
	scoreRect.SetPosition(wxPoint(w - scoreRect.GetWidth() - distance, distance));
	dc->SetPen(*wxBLACK_PEN);
	dc->SetBrush(*wxWHITE_BRUSH);
	dc->DrawRectangle(scoreRect);
	scoreRect.Deflate(5, 5);
	dc->DrawLabel(scoreLabel, scoreRect, wxALIGN_TOP|wxALIGN_CENTER_HORIZONTAL);
	dc->DrawLabel(scoreValue, scoreRect, wxALIGN_BOTTOM|wxALIGN_CENTER_HORIZONTAL);
}

void HabraSnakeView::DrawGameOverLabel(wxDC * dc, const wxRect & rect)
{
	wxFont font = dc->GetFont();
	font.SetPointSize(12);
	dc->SetFont(font);
	dc->SetTextForeground(wxColour(127,0,0));
	wxString label = _("GAME OVER");
	wxSize textSize = dc->GetTextExtent(label);
	dc->SetBackground(wxBrush(wxColour(200,200,200)));
	dc->SetPen(*wxBLACK_PEN);
	wxRect labelRect(rect.GetLeft() + (rect.GetWidth()-textSize.GetWidth())/2 - 10,
		rect.GetTop() + (rect.GetHeight()-textSize.GetHeight())/2 - 10,
		textSize.GetWidth()+20, textSize.GetHeight()+20);
	dc->DrawRectangle(labelRect);
	dc->DrawLabel(label, labelRect, wxALIGN_CENTER);
}

void HabraSnakeView::DrawSnake(wxDC * dc, wxRect & rect, 
	int rectSize, const wxPointList & snake)
{
	dc->SetPen(*wxTRANSPARENT_PEN);
	dc->SetBrush(wxBrush(wxColour(0,0,127)));
	wxRect cell(0,0,rectSize, rectSize);
	for(wxPointList::Node * node = snake.GetFirst(); node; node = node->GetNext())
	{
		wxPoint * point = node->GetData();
		if(!point) continue;
		cell.SetPosition(wxPoint(
			rect.GetLeft()+point->x*rectSize, 
			rect.GetTop()+point->y*rectSize));
		dc->DrawRectangle(cell);
	}
}

void HabraSnakeView::DrawTarget(wxDC * dc, wxRect & rect, 
	int rectSize, const wxPoint & target)
{
	dc->SetPen(*wxTRANSPARENT_PEN);
	dc->SetBrush(wxBrush(wxColour(0,150,50)));
	wxRect cell(rect.GetLeft()+target.x*rectSize,rect.GetTop()+
		target.y*rectSize,rectSize, rectSize);
	dc->DrawRectangle(cell);
}

wxRect HabraSnakeView::GetGameFieldRectangle(const wxSize & size, int rectSize)
{
	wxRect rect(0, 0, HabraSnakeDocument::GetGameFieldSize().GetWidth()*rectSize+1,
		HabraSnakeDocument::GetGameFieldSize().GetHeight()*rectSize+1);
	rect.Offset((size.GetWidth()-rect.GetWidth())/2,
		(size.GetHeight()-rect.GetHeight())/2);
	return rect;
}

void HabraSnakeView::DrawGrid( wxDC* dc, wxRect &rect, int rectSize )
{
	dc->DrawRectangle(rect);
	dc->SetPen(wxPen(wxColour(192,192,192)));
	for(int i = 1; i < HabraSnakeDocument::GetGameFieldSize().GetWidth(); i++)
	{
		dc->DrawLine(rect.GetLeft()+i*rectSize, rect.GetTop()+1,
			rect.GetLeft()+i*rectSize, rect.GetBottom());
	}
	for(int i = 1; i < HabraSnakeDocument::GetGameFieldSize().GetHeight(); i++)
	{
		dc->DrawLine(rect.GetLeft()+1, rect.GetTop()+i*rectSize,
			rect.GetRight(), rect.GetTop()+i*rectSize);
	}
}

void HabraSnakeView::RefreshScene()
{
	HabraSnakeCanvas * frame = wxDynamicCast(GetFrame(), HabraSnakeCanvas);
	if(frame)
	{
		frame->RefreshScene();
		frame->Refresh();
	}
}

Краткое описание приведенных выше методов:

  • Метод GetGameFieldRectangle() возвращает объект wxRect, содержащий координаты игрового поля на канве (прямоугольник центрировано по горизонтали и по вертикали).
  • Метод RefreshScene() позволяет отрисовать double-buffer и обновить/перерисовать канву.
  • Метод DrawGrid() отрисовывает игровое поле, а также все ячейки на нем.
  • Метод DrawTarget() отображает “цель” на игровом поле.
  • Метод DrawSnake() отображает змейку на игровом поле.
  • Метод DrawGameOverLabel() отображает надпись “GAME OVER” когда игра закончена.
  • Метод DrawScore() отображает набранные очки в правом верхнем углу канвы.
  • Виртуальный метод OnDraw(), унаследованный от wxView, вызывается канвой для отрисовки сцены на double-buffer’е. В зависимости от состояния игры, он вызывает остальные методы отрисовки составных частей сцены.

Обработка событий

Кроме отрисовки, класс представления позволяет выполнять обработку событий, т.к. является производным от wxEvtHandler. В нашем случае нам необходимо обеспечить постоянное перемещение змейки, т.е. обрабатывать сообщения от таймера, а также обеспечить реакцию на нажатие клавиш:
HabraSnakeView.h

class HabraSnakeView : public wxView
{
	typedef bool (HabraSnakeDocument::* MovementFunction)(bool & targetFound);
	...
	wxDirection m_CurrentDirection;
	wxTimer * m_GameTimer;
	void CreateGameTimer();
	void DoMove(HabraSnakeDocument * document, 
		MovementFunction func, wxDirection direction);
	...
public:
	...
	DECLARE_EVENT_TABLE()
	void OnKeyDown(wxKeyEvent & event);
	void OnGameTimer(wxTimerEvent & event);
};

Мы добавили два новых поля в класс HabraSnakeView:

  • m_CurrentDirection – направление движения змейки.
  • m_GameTimer – таймер для реализации автоматического перемещение змейки.

HabraSnakeView.cpp

BEGIN_EVENT_TABLE(HabraSnakeView, wxView)
EVT_KEY_DOWN(HabraSnakeView::OnKeyDown)
END_EVENT_TABLE()

HabraSnakeView::HabraSnakeView()
: m_CurrentDirection(wxLEFT)
{
	do 
	{
		...
		CreateGameTimer();
	} 
	while (false);
} 

HabraSnakeView::~HabraSnakeView()
{
	wxDELETE(m_GameTimer);
} 

void HabraSnakeView::CreateGameTimer()
{
	long gameTimerID = wxNewId();
	m_GameTimer = new wxTimer(this, (int)gameTimerID);
	Connect(gameTimerID, wxEVT_TIMER, 
		wxTimerEventHandler(HabraSnakeView::OnGameTimer));
	m_GameTimer->Start(300);
}

void HabraSnakeView::DoMove(HabraSnakeDocument * document, 
	HabraSnakeView::MovementFunction func, wxDirection direction)
{
	bool targetFound(false);
	if((document->*func)(targetFound))
	{
		m_CurrentDirection = direction;
		if(targetFound)
		{
			int interval = 350 - 15 * 
				wxMin(20, document->GetSnake().GetCount()-3);
			m_GameTimer->Start(interval);
		}
	}
}

void HabraSnakeView::OnKeyDown(wxKeyEvent & event)
{
	do 
	{
		HabraSnakeDocument * document = 
			wxDynamicCast(GetDocument(), HabraSnakeDocument);
		if(!document) break;
		if(document->IsGameOver()) break;
		switch(event.GetKeyCode())
		{
		case WXK_LEFT:
		case WXK_NUMPAD_LEFT:
			if(m_CurrentDirection == wxLEFT) return;
			DoMove(document, &HabraSnakeDocument::MoveLeft, wxLEFT);
			break;
		case WXK_RIGHT:
		case WXK_NUMPAD_RIGHT:
			if(m_CurrentDirection == wxRIGHT) return;
			DoMove(document, &HabraSnakeDocument::MoveRight, wxRIGHT);
			break;
		case WXK_UP:
		case WXK_NUMPAD_UP:
			if(m_CurrentDirection == wxUP) return;
			DoMove(document, &HabraSnakeDocument::MoveUp, wxUP);
			break;
		case WXK_DOWN:
		case WXK_NUMPAD_DOWN:
			if(m_CurrentDirection == wxDOWN) return;
			DoMove(document, &HabraSnakeDocument::MoveDown, wxDOWN);
			break;
		default:
			event.Skip();
			return;
		}
	} 
	while (false);
	RefreshScene();
}

void HabraSnakeView::OnGameTimer(wxTimerEvent & event)
{
	do 
	{
		HabraSnakeDocument * document = 
			wxDynamicCast(GetDocument(), HabraSnakeDocument);
		if(!document) break;
		MovementFunction func = NULL;
		switch(m_CurrentDirection)
		{
		case wxLEFT:
			func = &HabraSnakeDocument::MoveLeft;
			break;
		case wxUP:
			func = &HabraSnakeDocument::MoveUp;
			break;
		case wxRIGHT:
			func = &HabraSnakeDocument::MoveRight;
			break;
		case wxDOWN:
			func = &HabraSnakeDocument::MoveDown;
			break;
		default:
			return;
		}
		if(func)
		{
			DoMove(document, func, m_CurrentDirection);
			RefreshScene();
		}
	} 
	while (false);
}
  • OnKeyDown() – обработчик нажатия клавиш. В нем, в зависимости от направления движения змейки выбирается соответствующая функция перемещения из класса HabraSnakeDocument и и затем вызывается метод DoMove().
  • OnGameTimer() – обработчик события от таймера.
  • DoMove() – этот метод вызывает функцию перемещения и, если при перемещении змейка добралась до “цели”, то уменьшает интервал работы таймера, тем самым увеличивая скорость перемещения змейки.

В классе канвы нам также необходимо добавить обработчик нажатия клавиш в котором мы просто будем перенаправлять событие на обработку классу представления.
HabraSnakeCanvas.h

class HabraSnakeCanvas: public wxWindow
{   
    ... 
    void OnKeyDown( wxKeyEvent& event );
};

HabraSnakeCanvas.cpp

void HabraSnakeCanvas::OnKeyDown( wxKeyEvent& event )
{
	if(m_View)
	{
		m_View->ProcessEvent(event);
	}
}

После всех перечисленных выше манипуляций мы должны получить такую структуру классов:

Для того чтобы при запуске приложения автоматически запускалась новая игра, добавим такие строки в метод CreateControls() класса главной формы:
HabraSnakeMainFrame.h

void HabraSnakeMainFrame::CreateControls()
{
	...
	wxCommandEvent event(wxEVT_COMMAND_MENU_SELECTED, wxID_NEW);
	ProcessEvent(event);
	m_Canvas->SetFocus();
}

Ну вот, запускаем приложение и получаем такую картинку:

Но это еще не все. Т.к. wxWidgets – это кросс-платофрменная библиотека, то после небольших манипуляций с настройками проекта можно собрать его для работы под управлением Windows для десктопов. Как собрать wxWidgets-приложение для работы под управлением Windows NT/2000/XP/Vista можно узнать здесь. В результате получим что-то подобное.


Но и это еще не все! Нашу игру можно собрать еще и под Linux.

Скачать исходный код игры, а также CAB-инсталлятор для Windows Mobile можно здесь.

Previous ArticleNext Article
Технический директор IT-Dimension, компании-разработчика кросс-платформенного программного обеспечения

This post has 3 Comments

3
  1. Поправьте пожалуйста свой RSS генератор так, чтобы он кидал в RSS не всю статью а только первые один-два абзаца.

  2. А чем обусловлен выбор именно документ\вид концепции?
    Все таки для игр это не особо привычно:) Почему не просто окно и на нем рисовать?

  3. Тем что ты получаешь готовое решение, позволяющее тебе отделить мух от котлет данные от логики. Т.е. класс документа у тебя просто storage данных, который ничего не делает, просто хранит состояние. View обеспечивает полностью функционал игры, а канву можно лепить любую, хоть даже и на форме рисовать можно если скормишь ее в SetFrame() класса представления, просто с отдельным контролом удобнее, на форме ведь может еще что-то находиться помимо канвы. Т.е. все части приложения выполняют четко обусловленную функцию.

Leave a Reply

Your email address will not be published. Required fields are marked *

К.

Кроссплатформенная разработка для мобильных с Xamarin

Введение

Совсем недавно компания Xamarin анонсировала выход новой версии своего инструментария для кроссплатформенной разработки мобильных приложений, но вменяемых статей на русском по этой тематике так и нет. На Хабре появился небольшой обзор, не связанный с кодингом, там же была пара попыток рассказать об этом чуть подробнее, но дальше процесса создания Hello World приложения дело не зашло. А жаль. В этот раз мы попробуем исправить это досадное недоразумение.

Начало работы

Инсталлятор Xamarin устанавливает плагин к Visual Studio, который позволяет разрабатывать приложения для популярных мобильных платформ в привычном разработчику окружении. Также устанавливается отдельная среда разработки Xamarin Studio, которая, судя по всему, является модифицированной версией MonoDevelop. Мне привычнее работать в Visual Studio поэтому примеры в этой статье будут показаны с использованием именно этой среды разработки.

После установки в Visual Studio добавляются шаблоны проектов для мобильных приложений под Android и iOS (поддерживается создание как специализированных приложений для iPad и iPhone, так и универсальных приложений, а также приложений, использующих OpenGL). Для создания приложений для iOS в Visual Studio прийдется, правда, заплатить. Этот функционал доступен либо в режиме trial, либо в business-версии инструментария, а это стоит $999 в год.

После создания проекта мы получаем все тот же API для каждой платформы, который мы имеем при нативной разработке, но синтаксис будет на C#, к тому же есть возможность использовать базовые типы .NET Framework, синтаксический сахар и прочие плюшки .NET.

Разработка для Android

После создания Android-проекта мы получаем набор файлов, в котором есть класс главного окна и набор ресурсных файлов. После длительной работы в Eclipse немного раздражает название папок в PascalCase, но к этому можно довольно быстро привыкнуть. Также есть отличия в работе с файлами ресурсов. Для того, чтобы встроенный дизайнер окон понимал файлы ресурсов с лайаутами, у них расширение изменено на .AXML вместо привычного .XML в Eclipse. Это довольно сильно раздражает, особенно если рисовать лайауты в Eclipse, а потом переносить в Visual Studio в случае если Eclipse’овский дизайнер окон больше нравится.

Встроенный дизайнер окон мне лично показался неудобным. Медленный, часто валит всю IDE, я так и не понял, как в нем по-простому переключиться между XML-видом и UI. Здесь уж точно можно сказать что писано чужими для хищников. Я для себя решил, что в Eclipse мне удобнее, привычнее, да и на экран ноутбука в Eclipse помещается больше полезной информации. Может кому-то дизайнер Visual Studio и понравится больше, на вкус и цвет фломастеры разные.

Activities

Код на C# для Mono for Android очень схож с кодом на Java.

namespace XamarinDemo.Android
{
    [Activity(Label = "XamarinDemo.Android", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        int count = 1;

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);
            Button button = FindViewById<Button>(Resource.Id.MyButton);
            button.Click += delegate { button.Text = string.Format("{0} clicks!", count++); };
        }
    }
}

Отличия, которые сразу видны:

  • Для регистрации activity не надо ничего прописывать в манифесте. Для этого используется аннотация [Activity]
  • Для того, чтобы сделать activity стартовой, необходимо для аннотации [Activity] указать параметр MainLauncher. Это будет равносильно заданию action = android.intent.action.MAIN и category = android.intent.category.LAUNCHER в манифесте
  • Основные методы жизненного цикла activity (да и вобще все методы) имеют названия в PascalCase.
Элементы управления и обработка событий

Для получения ссылок на элементы управления, как и в Java-версии, используется метод FindViewById(). Очень порадовало наличие generic-версии этого метода, который возвращает объект нужного типа и позволяет избавиться от c-cast’ов.
Вместо listener’ов, а точнее, в дополнение к ним, для подключения обработчиков событий используются делегаты. Можно использовать и listener’ы, но это, во-первых, не .NET way, во-вторых, требует написания большего количества кода, даже по сравнению с Java. К тому же делегатов можно подключить несколько:

Button button = FindViewById<Button>(Resource.Id.MyButton);
button.Click += delegate { button.Text = string.Format("{0} clicks!", count++); };
button.Click += (o, e) => 
    {
        Toast.MakeText(this, string.Format("{0} clicks!", count++), ToastLength.Long).Show(); 
    };

Не со всеми обработчиками событий в виде делегатов дела обстоят радужно. В одном из проектов обнаружилась проблема с событием View.ViewTreeObserver.GlobalLayout, у которого при использовании оператора -= не отключались делегаты-обработчики. Да вообще никак не отключались. Пришлось использовать IOnGlobalLayoutListener.

Использование Java-библиотек в .NET-проекте

В Mono for Android есть возможность использовать существующие JAR-файлы в приложении на C#. Для этих целей предусмотрен специальный тип проекта: Java Binding Library.
Для того, чтобы использовать JAR-библиотеку необходимо:

  • Создать проект Java Binding Library в решении.
  • В папку Jars положить нужный JAR-файл (в JAR-библиотеке не должно быть AndroidManifest.xml, ибо наличие этого файла может повлечь ряд неприятных последствий. Подробнее об этом можно узнать здесь).
  • Указать Build Action для JAR-файла как EmbeddedJar.
  • В проекте Android-приложения добавить в References проект созданной Java Binding Library.

После этого можно использовать классы из JAR-файла. Необходимо учитывать, что имена пакетов в C# и в исходном Java-коде могут немного отличаться. Так, например, пакет com.example.androiddemolib из Java-кода будет переименован на Com.Example.Androiddemolib (то есть будет преобразован в PascalCase).
Неплохую инструкцию по использованию Java-библиотек можно почитать здесь.

Работа с базами данных

Для работы с базами данных в Mono for Android предусмотрены используются классы пространства имен Mono.Data.Sqlite (используются для доступа к базам данных SQLite) и System.Data.SqlClient (для доступа к Microsoft SQL Server). Классы пространства имен System.Data.SqlClient доступны только в Business-редакции инструментов разработки. Можно также использовать классы-обертки над родным Java API для Android и сторонние разработки, например sqlite-net, в котором доступен асинхронный вариант API.
Пользоваться доступным API довольно просто. Все очень схоже с разработкой для настольных ПК:

namespace XamarinDemo.Android
{
    [Activity(Label = "XamarinDemo.Android", 
        MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        SqliteConnection GetConnection(string path)
        {
            SqliteConnectionStringBuilder builder = 
                new SqliteConnectionStringBuilder();
            if (!File.Exists(path))
            {
                FileInfo info = new FileInfo(path);
                if (!Directory.Exists(info.Directory.FullName))
                {
                    Directory.CreateDirectory(info.Directory.FullName);
                }
                SqliteConnection.CreateFile(path);
            }
            builder.DataSource = path;
            return new SqliteConnection(builder.ToString());
        }

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);

            try
            {
                string path = GetDatabasePath("xamarindemo.sqlite").Path;
                SqliteConnection connection = GetConnection(path);
                connection.Open();
                SqliteCommand command = connection.CreateCommand();
                command.CommandType = CommandType.Text;
                command.CommandText = "CREATE TABLE IF NOT EXISTS DemoTable(" +
                    "id INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL" +
                    ", name VARCHAR(32))";
                command.ExecuteNonQuery();
                connection.Close();
            }
            catch (Exception e)
            {
                System.Diagnostics.Debug.WriteLine(e.ToString());
            }
        }
    }
}

Разработка для iOS

Элементы управления и обработчики событий

Создание элементов управления в коде ничем не отличается от аналогичной работы в Objective-C. Обработчики событий можно навешивать также как и в Android – с помощью делегатов. Также можно добавлять обработчики как в Objective-C через селекторы.

namespace XamarinDemo.iOS
{
    public class MyViewController : UIViewController
    {
        UIButton button;
        …

        public MyViewController()
        {
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            …
            button = UIButton.FromType(UIButtonType.RoundedRect);
            …
            button.AddTarget(this, 
            	new Selector("ButtonTouchInside"), 
            	UIControlEvent.TouchUpInside);
            …
            button.TouchUpInside += (object sender, EventArgs e) =>
            {
                button.SetTitle(String.Format(
            		"clicked {0} times", numClicks++), UIControlState.Normal);
            };
            …

            View.AddSubview(button);
        }

        [Export("ButtonTouchInside")]
        void OnButtonTouchInside()
        {
            Console.WriteLine("Hello!");
        }

    }
}
Использование нативных библиотек

В Xamarin iOS, также как и для Android, доступна возможность использования нативных библиотек. Для этих целей есть специальный тип проекта – iOS Binding Project, в который можно добавлять статические библиотеки, после чего они будут слинкованы вместе с основным проектом.

В общем виде, для использования нативной библиотеки в C# проекте необходимо сделать следующее:

  • Создать проект статической библиотеки в XCode.
  • Если планируете отлаживать C# проект в симуляторе, то в настройках проекта статической библиотеки необходимо добавить архитектуру i386 в список Valid Architectures.
  • Написать нужную логику (допустим у нас есть один экспортируемый класс):
    @interface iOSDemoClass : NSObject
    - (NSInteger) addData: (NSInteger) value1 andOtherValue: (NSInteger) value2;
    @end
    …
    @implementation iOSDemoClass
    -(NSInteger) addData: (NSInteger) value1 andOtherValue: (NSInteger) value2
    {
        return value1 + value2;
    }
    @end
    
    
  • Закрыть проект в Xcode
  • Собрать раздельно статическую библиотеку для нужных архитектур (например таким образом):
    xcodebuild -project iOSDemoLib.xcodeproj \
    -target iOSDemoLib -sdk iphonesimulator \
    -configuration Release clean build
    xcodebuild -project iOSDemoLib.xcodeproj \
    -target iOSDemoLib -sdk iphoneos -arch armv7 \
    -configuration Release clean build
    
  • Сделать универсальную библиотеку для нескольких платформ из библиотек, получившихся на предыдущем шаге:
    lipo -create -output ../iOSDemoLib.Binding/libiOSDemoLib.a ../libiOSDemoLib-i386.a ../libiOSDemoLib-iphone-armv7.a
    
  • Создать проект iOS Binding Project в C#-решении и добавить туда универсальную статическую библиотеку. После этого будет автоматически создан файл [libname].linkwith.cs с правилами линковки, которые выглядят приблизительно так:
    [assembly: LinkWith ("libiOSDemoLib.a", LinkTarget.Simulator | LinkTarget.ArmV7, ForceLoad = true, Frameworks = "Foundation")]
    
  • В файле ApiDefinition.cs (создается автоматически) прописать правила привязки нативных классов к .NET-типам (обращаю внимание на то что комплексные имена методов должны содержать двоеточия в местах, где в Objective-C должны быть параметры метода. То есть в методе с двумя параметрами будет два двоеточия):
    namespace iOSDemoLib.Binding
    {
        [BaseType (typeof(NSObject))]
        interface iOSDemoClass 
        {
            [Export("addData:andOtherValue:")]
            int AddDataAndOtherValue(int value1, int value2);
        }
    }
    
  • В проекте приложения, в котором планируется использовать библиотеку, добавить проект iOS Binding в References.
  • Собрать, запустить, быть счастливым.

Вообще, очень хорошо и доступно о привязке нативных библиотек написано здесь.

Работа с базами данных

Для работы с базами данных в iOS используются те же пространства имен, что и в Android. API, соответственно, такой же. Небольшая разница может быть в мелочах. Например, для получения пути к файлу базы данных SQLite в Android есть специальный API-вызов:

string path = GetDatabasePath("xamarindemo.sqlite").Path;

В iOS же нужно использовать стандартные средства .NET:

string path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Personal), 
       "xamarindemo.sqlite");
Удаленная отладка iOS приложений из Visual Studio

В Xamarin 2.0 есть возможность отладки мобильных приложений для iOS прямо из Visual Studio. Для того, чтобы это стало возможно, необходимо иметь в локальной сети Mac с установленным XCode, iOS SDK и Xamarin Studio. Никаких дополнительных настроек производить не надо, достаточно при открытии iOS проекта в Visual Studio выбрать нужный build-сервер из списка доступных.

К сожалению данный подход не заработал с виртуальной машиной, запущенной на том же компьютере что и Visual Studio. Хотя с обычным Mac’ом в локальной сети все замечательно работает. Причин или объяснений этому пока найти не удалось, пытаюсь общаться с разработчиками по этому поводу.

Также не очень понятно, как организовывать UI-тестирование и проверку работоспособности приложения из Visual Studio. Симулятор запускается на Mac’е, похоже, что без VNC здесь не обойтись.

Кроссплатформенные библиотеки классов (Portable Class Libraries)

Xamarin предоставляют возможность создания библиотек, которые могут быть использованы (в виде кода или готовых сборок) сразу для нескольких платформ. Такие библиотеки называются Portable Class Library (PCL).

Для разработки подобных библиотек используется специальный урезанный вариант .NET Runtime и это чудесный инструмент для разработчика, значимость которого трудно переоценить, но здесь тоже не все так просто. По умолчанию в Visual Studio нельзя указать Android и iOS в качестве поддерживаемых платформ для PCL-проекта. Но это не значит, что Visual Studio сразу становится бесполезной в этом плане.

Проблему можно решить путем создания XML-файлов в папке C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.0\Profile\Profile104\SupportedFrameworks для x64 систем или в папке C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.0\Profile\Profile104\SupportedFrameworks для x86 систем.
Android файл MonoAndroid,Version=v1.6+.xml

<?xml version="1.0" encoding="utf-8"?>
<Framework DisplayName="Mono for Android"
  Identifier="MonoAndroid"
  Profile="*"
  MinimumVersion="1.6"
  MaximumVersion="*" />

iOS файл VSMonoTouch,Version=v1.0+.xml

<?xml version="1.0" encoding="utf-8"?>
<Framework DisplayName="VS MonoTouch"
  Identifier=".NETFramework"
  Profile="*"
  MinimumVersion="1.0"
  MaximumVersion="1.0" />

После создания этих двух файлов и перезапуска Visual Studio можно будет указывать Android и iOS в качестве поддерживаемых платформ для новых PCL-проектов или добавить эти платформы в существующие проекты через Properties -> Library -> Target Framework.
Но это еще не все нюансы.

Если вы хотите поддержку Android и iOS в PCL-проекте, то придётся отказаться от поддержки Xbox 360 и Windows Phone 7 (зато можно поддерживать Windows Phone 7.5+). Для Xbox придётся создавать еще один проект и в нем добавлять файлы из существующего PCL-проекта в виде ссылок. Или, как вариант, в одном PCL-проекте оставить все платформы от Microsoft (включая Xbox 360), а в другом оставить только iOS и Android.

Есть еще такая проблема что PCL-проект, добавленный из Visual Studio под Windows не будет участвовать в сборке решения, если открыть его в Xamarin Studio под OS X. Проект будет неактивен и будет выводиться сообщение (not built in active configuration). Решается эта проблема удалением проекта из решения и добавлением заново.

Новые возможности языка и поддержка C#5

В разработке для Android актуальным является вынесение работы с файлами, базами данных, сетью и т.д., то есть с длительными операциями, в отдельный поток. В C# 5 для реализации асинхронных операций предусмотрены специальные возможности, а именно async/await. К сожалению, в текущей версии Mono for Android и MonoTouch этих возможностей нет. Из-за этого многие довольно интересные библиотеки нельзя использовать в том виде, который для них предусмотрен. Например для работы с асинхронным API библиотеки sqlite-net приходится делать несколько финтов ушами. Радует то, что эти возможности должны стать доступны через несколько месяцев с переходом Xamarin на C# 5.

Дополнительные компоненты и библиотеки

Xamarin помимо, собственно, продажи инструментария для разработки ПО, открыли магазин по продаже сторонних компонент, многие из которых действительно очень упрощают жизнь разработчику. В магазине доступны как платные, так и бесплатные библиотеки и темы оформления. Полезность, например, бесплатных Xamarin.Auth, Xamarin.Social и Xamarin.Mobile трудно переоценить. Есть возможность публиковать собственные компоненты в этом магазине.

Неприятные моменты

Из проблемных моментов наиболее заметными являются:

  • Жуткие тормоза при отладке Android-приложений, когда на довольно мощной машине с Core i7 и 8 GB RAM при останове на брейкпоинте Visual Studio зависает на 10-15 секунд. Подобное может повторяться на каждом шаге отладчика.
  • Отсутствие в списке устройств, доступных для отладки приложений, некоторых устройств, подключенных к компьютеру (которые, тем не менее, видны через adb devices и на которых Eclipse отлично запускает приложения)
  • Произвольные отпадания отладчика и необходимость перезапуска приложения в связи с этим.
  • Отсутствие статей и учебных материалов (да, есть какие-то материалы на официальном сайте, но они покрывают не все популярные задачи)
  • Наличие багов, о которых узнаешь ВНЕЗАПНО! после общения на форуме. Хотя на том же официальном форуме разработчики довольно бодро отвечают на вопросы и вообще молодцы.
  • Цена на Business-редакцию очень кусается. К тому же лицензия на год, а не навсегда (немного скрашивает ситуацию наличие скидок при покупке лицензии сразу для нескольких платформ).

Приятные моменты

  • При покупке лицензии сразу для нескольких платформ или сразу для нескольких рабочих мест, предусмотрена скидка.
  • С выходом новой версии, в которой будет поддержка async\await разработка действительно станет проще и будет возможность использовать кучу полезных библиотек, не меняя их код.

Выводы

Инструментарий от Xamarin таки да, действительно работает и если вы планируете разрабатывать несколько приложений в год или же планируемая прибыль от разрабатываемого приложения больше чем $2k, то Xamarin SDK явно может облегчить вам жизнь и избавить от двойной работы над кодом для каждой платформы.

С другой же стороны, для Indy-разработчика цена в $1k для каждой платформы мне, например, кажется чрезмерной т.к. есть много нюансов, которые необходимо знать и\или прочувствовать на себе перед началом разработки, есть баги, которые неизвестно в каком режиме будут исправляться и неизвестно насколько их наличие может замедлить разработку конкретно вашего проекта.

Немного клёвых ссылочек!

UPD: Уточнение по поводу стоимости лицензии:

Лицензия бессрочная, но включает в себя один год бесплатных обновлений. Я им писал в саппорт за разъяснениями. Вот их ответ

Your license is perpetual. You have to purchase a renewal every year to get new updates.

W.

Windows® Marketplace for Mobile Developer Strategy

Сегодня Microsoft опубликовала информацию о том, как будет функционировать online-сервис продажи мобильных приложений Windows® Marketplace for Mobile.

Итак, информация к размышлению:

  1. Сколько будет зарабатівать разработчик на продаже своих приложений?
    • Разработчик будет получать 70% от продаж в Windows Marketplace for Mobile (на сколько я понимаю, процент сравним с AppStore от Apple).
    • Приложение может продаваться на 29 торговых площадках (markets) с ценовым разграничением по каждой из них.
    • Также приложение может распространяться бесплатно, т.е. в Windows Marketplace for Mobile можно будет запостить и бесплатные приложения.
  2. Что нужно для регистрации?
    • Информация о регистрации будет доступна чуть позже (весной). Прием приложений планируется начать к лету.
    • Разработчики смогут выкладывать 5 приложений ежегодно за $99. И еще прийдется платить по $99 за каждое дополнительное приложение.
    • Для студентов, участвующих в программе DreamSpark, цены будут значительно снижены.
  3. Что нужно для того, чтобы приложение попалов Marketplace?
    • Сказано, что значительное внимание будет уделено совместимости и корректной работе приложений на мобильных устройствах. Планируется организовать процесс сертификации и тестирования приложений, выкладываемых в Marketplace.
    • разработчикам будет предоставляться детальная информация о результатах сертификации на Windows Marketplace for Mobile developer portal.
  4. Что нужно для того, чтобы начать разработку для Windows Mobile?
    • Можно использовать Visual Studio и .NET Compact Framework 3.5 (я так понимаю, они это говорят в рекламных целях, C++ еще вроде никто не отменял).
    • Скачать Windows Mobile 6.0 SDK и ознакомиться с информацией на http://developer.windowsmobile.com.

Ознакомиться с пресс-релизом можно здесь.

Интервью с Inigo Lopez, Marketplace Product Manager: