第一个程序是series60 SDK自带的hello world程序,在group目录下将有下面的工程文件,bld.inf、s60test.mmp、及由bldmake生成的abld.bat文件,有关编译的命令可以查看相关资料。
第一个程序是series60 SDK自带的hello world程序,在group目录下将有下面的工程文件,bld.inf、s60test.mmp、及由bldmake生成的abld.bat文件,有关编译的命令可以查看相关资料。
Groupstep1.rss是资源文件,在我们的例子中包含软键盘的定义(R_AVKON_SOFTKEYS_OPTIONS_EXIT-右边的选择键及左边的退出键)和选择菜单,还可以在后面加更多的资源.
Groupstep1.pkg描述如何去创建*.sis文件,*.sis是可以安装在手机中的文件。
inc和src包含程序的源代码,hello world在SDK中有详细的描述,我在这里主要讲一些主要的。
不像windows和UNIX程序,symbian程序没有带main()函数,可以像动态链接被系统装载,像其他每个动态链接它有e32dll函数,但是必须被迅速归还。
GLDEF_C TInt E32Dll(TDllReason /*aReason*/)
{
return KErrNone;
}
系统调用newapplication()函数得到新的CApaapplication对象,
EXPORT_C CApaApplication* NewApplication()
{
return (new CS60TestApplication);
}
在avkon(for series60)中返回一个CAknapplication子类的对象,在这个例子中是在s60testapplication.cpp实现的CS60testapplication,在每次执行中下面两个函数必须重载,第一个,在AppDllUid将返回该程序唯一的UID,我们例子中的UID不会出现在实际发布的程序中.
第二个函数是创建CApaDocument类对象的CreateDocumentL函数,
CApaDocument* CS60TestApplication::CreateDocumentL()
{
CApaDocument* document = CS60TestDocument::NewL(*this);
return document;
}
TUid CS60TestApplication::AppDllUid() const
{
return KUidS60TestApp;
}
在我们的例子中是CAknDocument的继承类CS60TestDocument
class CS60TestDocument : public CAknDocument
{
public:
static CS60TestDocument* NewL(CEikApplication& aApp);
static CS60TestDocument* NewLC(CEikApplication& aApp);
~CS60TestDocument();
CS60TestAppUi *iAppUi;
public: // from CAknDocument
CEikAppUi* CreateAppUiL();
private:
void ConstructL();
CS60TestDocument(CEikApplication& aApp);
};要重载CreateAppUil,此函数是用来建立用户接口响应对象的,
CEikAppUi *CS60TestDocument::CreateAppUiL()
{
iAppUi=new(ELeave) CS60TestAppUi(this);
return iAppUi;
}
在我们的例子中这是由类CS60TestAppUi实现的,在该类中ConstructL函数首先调用BaseConstructL函数进行初始化,从资源中装载软键盘和菜单定义,
void CS60TestAppUi::ConstructL()
{
BaseConstructL();
iAppView=CS60TestAppView::NewL(ClientRect(), iDoc);
AddToStackL(iAppView);
}
接下来我们将创建类CS60TestAppView的对象,这个类是CCoeControl的继承类,
class CS60TestAppView : public CCoeControl
{
..........
}
CCoeControl对象将控制哪个在屏幕中描绘,我们的ClientRect()控制将填充状态栏与软键盘间的空间,把它改成ApplicationRect()将控制全屏,AddToStackL接受来自键盘的反应,AppUi对象同样接受来自菜单的反应,当用户选择了菜单命令HandleCommandL将调用相应的命令代码,将完成结束命令和"Hello"命令显示一段文本。
void CS60TestAppUi::HandleCommandL(TInt aCommand)
{
switch(aCommand)
{
case EEikCmdExit:
case EAknSoftkeyExit:
Exit();
break;
case ES60TestHell
{
_LIT(message, "Hello!");
CAknInformationNote *informationNote=new(ELeave) CAknInformationNote;
informationNote->ExecuteLD(message);
}
break;
default:
Panic(ES60TestBasicUi);
break;
}
}
Draw函数是我们在CS60TestAppView唯一重载的函数,它将当我们的视图需重画时被调用,在本例中我们将显示在(176*144)的区域中,定义字符串将用到TBuf类。
在这一步中我将加入游戏需要的数据结构。
这里我们又加入了两个类TBlock和TGrid,这两个类没有特定的数据类型,它们是T型类,TBlock指向一个单个的俄罗斯方块(由四个小方块组成),TGrid指向已由(20*10)小方块填充的格子。在document类中,iGrid包含当前面板,iCurrBlock包含正在下落的方块,iBlockPos是正在下落方块的位置。
本例加的主要是比特的位操作,symbian OS特殊之处是用了TFixedArray类,它象普通的类被利用,但是内部会有下标检查,当在TFixedArray中下标是20时不会有内存溢出而是抛出错误。
第三步:加入用户接口
这一步我们将加入用户接口来测试第二步中加入的数据结构,我们可以用方向键移动方块到想要的位置然后用OK来固定方块(因为刚开始方块在顶部,先按向下的方向键,才能看到方块),玩家可以旋转方块。
首先用CS60TestAppView::Draw函数画背板,
void CS60TestAppView::Draw(const TRect& /*aRect*/) const
{
CWindowGc &gc=SystemGc();
TRect rect=Rect();
gc.Clear(rect);
int i, j;
TFixedArray<TInt8, KGridX> arr;
gc.SetPenColor(TRgb(0));
gc.SetBrushStyle(CWindowGc::ESolidBrush);
for (i=0; i<=KGridY; i++)
gc.DrawLine(TPoint(KBoardOffset, KBoardOffset+KCellSize*i),
TPoint(KBoardOffset+KGridX*KCellSize, KBoardOffset+KCellSize*i));
for (i=0; i<=KGridX; i++)
gc.DrawLine(TPoint(KBoardOffset+KCellSize*i, KBoardOffset),
TPoint(KBoardOffset+KCellSize*i, KBoardOffset+KGridY*KCellSize));
for (i=0; i<KGridY; i++)
{
iDoc->GetRowContent(i, arr);
for (j=0; j<KGridX; j++)
{
gc.SetBrushColor(KColors[arr[j]]);
if (arr[j])
gc.DrawRect(TRect(KBoardOffset+KCellSize*j, KBoardOffset+KCellSize*i,
KBoardOffset+KCellSize*(j+1)+1, KBoardOffset+KCellSize*(i+1)+1));
}
}
}
我们用TGrid获得方块类型,用DrawLine和DrawRect来画背板,用SetPenColor来控制边框和线条的颜色,用SetBrushStyle/SetBrushColor来控制背板小方块的颜色,所以CWindowGc的方法可以查看SDK帮助文件。
我们需要对每个按键起作用,每次按键产生一个事件,该事件首先送给CS60TestAppView处理,将压到AddStackL栈顶部,缺省执行是返回EKeyWasNoConsumed,接下来此事件将送给CS60TestAppUi处理,在这个类中将用HandlKeyEventL来处理对应的按键.
KeyResponse CS60TestAppUi::HandleKeyEventL(const TKeyEvent &aKeyEvent,
TEventCode aType)
{
if (aType==EEventKey)
{
if (aKeyEvent.iCode==EKeyUpArrow)
if (iDoc->iBlockPos.iY>0)
iDoc->MoveBlock(iDoc->iBlockPos-TPoint(0, 1));
if (aKeyEvent.iCode==EKeyDownArrow)
iDoc->MoveBlock(iDoc->iBlockPos+TPoint(0, 1));
if (aKeyEvent.iCode==EKeyLeftArrow)
iDoc->MoveBlock(iDoc->iBlockPos-TPoint(1, 0));
if (aKeyEvent.iCode==EKeyRightArrow)
iDoc->MoveBlock(iDoc->iBlockPos+TPoint(1, 0));
if (aKeyEvent.iCode==EKeyDevice3)
{
if (iDoc->FixBlock())
iDoc->NewBlock();
}
if (aKeyEvent.iCode=='1')
iDoc->RotateBlock(-1);
if (aKeyEvent.iCode=='0' || aKeyEvent.iCode=='3')
iDoc->RotateBlock(1);
}
return EKeyWasNotConsumed;
}
但方格有变化时,我们将重画屏幕,CS60TestAppUi::UpdateBroad里的DrawDeferred来刷新整个屏幕。
好的设计是对每个按键都用EKeyWasConsumed来响应,我们会用EKeyWasNotConsumed来处理一些无用的按键。
最后在菜单里假如"new game"选项.
MENU_ITEM {command = ES60TestNewGame; txt = "New Game";}
当前游戏还不是一个有趣的游戏,用户可以移动方块到他想要的地方,这样就很无聊,这一步我们将加入游戏引擎,它将使方块自由下落。
这个引擎类是CTimer类的继承类CS60TestEngine,我将用After(iInterval)将引擎挂起一段时间,至少要隔iInterval微秒后,再运行CS60TestEngine::RunL,如果用一个循环延迟时间来取代CTimer,这样将要中断主线程,不能接收按键事件和显示菜单。
CTimer是一个需激活对象,我们用CActiveScheduler::Add(this)将它加入时间表队列。
void CS60TestEngine::ConstructL()
{
CTimer::ConstructL();
CActiveScheduler::Add(this);
After(iInterval);
iState=ERunning;
}
但用户重新玩游戏,将先用Cancel()来结束,在隔一定时间后重新开始.
void CS60TestEngine::Reset()
{
if (iState==ERunning)
Cancel();
iState=ERunning;
After(iInterval);
}
在RunL里,方块延一条线下坠,但它不能再下坠时我们将固定此方块,再产生新的方块,
void CS60TestEngine::RunL()
{
if (!iDoc->MoveBlock(iDoc->iBlockPos+TPoint(0, 1)))
{
if (!iDoc->FixBlock())
{
// Game over
TBuf<64> message;
CEikonEnv::Static()->ReadResource(message, R_NOTE_GAME_OVER);
CAknInformationNote *informationNote=new(ELeave) CAknInformationNote;
informationNote->ExecuteLD(message);
iState=EGameOver;
return;
}
iDoc->CheckRows();
if (iDoc->iLevel<=(iDoc->iLines/10))
{
iInterval*=3;
iInterval/=4;
iDoc->iLevel++;
}
iDoc->NewBlock();
}
iBeginTime.HomeTime();
After(iInterval);
}
但不能再放方块时,我们将结束游戏,并显示一段文字
CEikonEnv::Static()->ReadResource(message, R_NOTE_GAME_OVER)
结束游戏引擎
iState=EGameOver。
我们将在资源文件中加载"game over",这样我们只要翻译资源文件就可将游戏翻译成不同的语言,s60test.rss在加入TBUF型字符串
RESOURCE TBUF32 r_note_game_over
{
buf = "Game Over";
}
Build 将其建成s60test..rsg文件,在这个文件中R_NOTE_GAME_OVER定义成ID,通过
CEikonEnv::Static()->ReadResource(message, R_NOTE_GAME_OVER)
来加载"game over"
第五步
我们已经基本完成游戏,但是在几个方面还要改进。
第一个是用户打开其它程序或打开菜单,游戏仍在继续,当他回来继续玩的时候,游戏可能已经结束了,为避免这样因此我们应加入暂停的功能。
暂停/停止暂停的功能用户将会在暂停的时候用到,这时将要修改选择的菜单,TechPause/TechUnPause将会被用户切换到其他程序或菜单(不改变菜单选项)时调用,
void TechPause() { iTechPauseRef++; DoPause(); }
void TechUnpause() { iTechPauseRef--; DoPause(); }
这两个都参考了计数器,调用两次TechPause,将调用两次TechUnPause来停止暂停游戏,这是以前老版本游戏的用法,本游戏不是这样的
void CS60TestAppView::FocusChanged(TDrawNow aDrawNow)
{
if (IsFocused())
{
if (!iFocus)
{
iFocus=true;
iEngine->TechUnpause();
}
} else
{
if (iFocus)
{
iFocus=false;
iEngine->TechPause();
}
}
}
如果在DoPause里进行暂停和停止暂停,在暂停是我们要计算暂停多长时间,并结束计数器,
void CS60TestEngine::DoPause()
{
__ASSERT_ALWAYS(iPauseRef>=0 && iTechPauseRef>=0, Panic(ES60TestAssert));
if (iPauseRef==0 && iTechPauseRef==0)
{
if (iState==EPaused)
{
int ms=iPauseTime.MicroSecondsFrom(iBeginTime).Int64().GetTInt();
if (ms<0 || ms>iInterval)
ms=0;
iState=ERunning;
After(iInterval-ms);
}
} else
{
if (iState==ERunning)
{
iState=EPaused;
iPauseTime.HomeTime();
Cancel();
}
}
}
在停止暂停时我们同样要一个计数器来计算剩余的时间。
我们调用TechPause/TechUnPause CS60TestAppView::FocusChanged时是我们打开其他程序或菜单也就是我们的焦点不在此游戏上是,而Pause/UnPause CS60TestAppUi::HandleCommandL在菜单选项里选择的。
当用户从菜单里选择"pause"后,我们应将菜单改成"unpause"转态,这是通过CS60TestAppUi::DynInitMenuPaneL来实现的,每次显示菜单是都会执行它。因此我们在资源文件中设定相对应的字符串。
RESOURCE TBUF16 r_menu_pause_title
{
buf = "Pause";
}
RESOURCE TBUF16 r_menu_unpause_title
{
buf = "Unpause";
}
另外我们还在背景里加了一副图片,
图片在symbian OS中被存为*.mbm文件,是从*.bmp文件在build过程时制作过来的,在mmp文件加入
START BITMAP S60Test.mbm
HEADER
TARGETPATH systemappsstep5
SOURCEPATH .. itmaps
SOURCE c12 tlo.bmp
END
就可以了,一个*.mmp文件可以包含几个*.bmp文件
在本例中s60test.mmp将包含一个bmp文件,图片前面的c12表示是12bit(4096色)来节省空间,你也可以用C16(65536色),build也可以创建另外一个文件s60test.mbg,它将包含所有*.mbm文件的ID,在我们的文件就一个,所以它的ID是EMbmS60testTLO,
iBackground=CEikonEnv::Static()->CreateBitmapL(iPathName, EMbmS60testTlo);
来装载背景图片,
我们的游戏需要一个很好的图标和名字,这些都可以通过AIF文件实现,主要就是在资源文件S60testaif.rss中定义AIF_DATA数据,
#include <aiftool.rh>
RESOURCE AIF_DATA
{
app_uid=0x04545FF6;
caption_list=
{
CAPTION
{
code = ELangEnglish;
caption = "Tetris";
}
};
num_icons=2;
embeddability=KAppNotEmbeddable;
newfile=KAppDoesNotSupportNewFile;
}
// End of File
除了这个文件我们还需要四张图片,44*44bitmap,44*44bitmask,44*23bitmap,44*23bitmask,图片用不同的颜色,可以辨别什么时候用的什么图片,AIF文件需要在*.mmp文件里通过AIF命令引用
AIF Step6.aif ..Aif S60TestAif.rss
c12 Icon.bmp IconMask.bmp IconSmall.bmp IconSmallMask.bmp
CTIC.川科创新 3G嵌入式技术教育专家(学3G送手机)
3G手机软件工程师培训班 现热招中 报名即 送3G手机 一部
400-702-8828 www.ctic.cc