スネークゲーム2

スネークゲーム 7

イベントハンドリング

 イベントの発生を処理できるようにすると、
queue(記憶先入先出しシステム)で演算した後、
イベント処理関数へ到達する。
アップデートMQL4は、
OnChartEvent関数があり、
通常のカスタムイベントと同様、
チャートで動作する為のイベントハンドリングを生成する。
各イベントは、
OnChartEvent関数に渡すための確認処理ツールと
パラメータを持っている。
処理のスレッド(つながり)が、
プログラムの全ての関数の外にあるときにのみ、
OnChartEvent関数は呼ばれる。
 
 次の例では、
OnChartEventはけしてコントロールされない。
なぜならwhileの無限ループが、
MyFunction関数から出ることを許さないので、
OnChartEvent関数はコントロール出来ず、
ボタンを押してもArert関数を呼ぶことが出来ない。

#include

void MyFunction()
{

CChartObjectButton *button;
button = new CChartObjectButton;
button.Create(0,"button",0,10,10,100,20);
button.Description("Button text");

while(true)
{
"The code,that should be called periodically"
}

}

int OnInit()
{
MyFunction();
return(0);
}

void OnChartEvent(const int id,
                  const long   & lparam,
                  const double & dparam,
                  const string & sparam)
{

if(id = CHARTEVENT_OBJECT_CLICK && sparm == "butoon") Alert("Button click");

}


イベントハンドリングの断続的コード実行

 スネークゲームでは、時間周期のイベントハンドリングによる、
確実なスネーク動作関数の断続的な呼び出しが必要とされる。
しかし、上記の例ではエンドレスループにより、
OnChartEventが呼ばれず、イベントハンドリングが不可能となる。
すなわち他の方法で、断続的実行を考案する必要がある。



スネークゲーム 8

OnTimerの使用


アップデートMQL4、MQL5言語は、特殊なOnTimer関数により、
予め定義された秒数でEventSertTimer関数が呼ばれ、
断続的に実行される、

前回の例は次のように書き直される。


#include

void MyFunction()
{

"The code,that should be called periodically"

}


int OnInit()
{


CChartObjectButton *button;
button = new CChartObjectButton;
button.Create(0,"button",0,10,10,100,20);
button.Description("Button text");

EventSetTimer(1);

return(0);

}

void OnTimer()
{

MyFunction();

}

void OnChartEvent(const int id,
                  const long   & lparam,
                  const double & dparam,
                  const string & sparam)
{

if(id = CHARTEVENT_OBJECT_CLICK && sparm == "butoon") Alert("Button click");

}

OnInit関数で、ボタンを生成し、そして1秒間隔の周期を定義する。
OnTimer関数の呼び出しは、1秒毎に行われ、MyFunction関数のコードを
周期的に実行する。
OnTimer関数の呼び出しは、複数回であることに注意。
ミリセコンドで記述された関数は。他のメソッドが必要です。
そのメソッドはカスタムイベントでの使用となります。
*カスタムイベントは次回に説明


スネークゲーム 9

カスタムイベントの使用

 カスタムイベントはEventChartCustom関数により生成されます。
イベントIDとそのパラメータが、
EventChartCustom関数のインプットパラメータで定義されます。
IDを定義するカスタム番号は0から65535までで、65536個あります。
MQL4、MQL5のコンパイラは、他のタイプのイベントからカスタムイベントを
識別するため、CHARTEVENT_CUSTOM定数アイデンテイファイアをIDへ自動的に加えます。
このようなカスタムIDの実際のレンジ幅は、
CHARTEVENT_CUSTOMから、
CHARTEVENT_CUSTOM+65535(CHARTEVENT_CUSTOM_LAST)までです。

 
下記は、カスタムイベントを使用したMyFunctionを周期的に呼び出す例です。

void MyFunction()
{
Sleep(200);
EventChartCustom(0,0,0,0," ");
}

int OnInit()
{
CChartObjectButton *button;
button = new CChartObjectButton;
button.Description("Button text");
return(0);
}

void OnChartEvent(const int id,
                 const long &lparam,
                 const double &dparam,
                 const string &sparam)
{
if(id == CHARTEVENT_OBJECT_CLICK && sparam == "button") Arart("Button click");
if(id == CHARTEVENT_CUSTOM) MyFunction();

}

 この例では、200ms遅れたMyFunction関数(この関数を呼ぶ周期時間)と、
カスタムイベントが生成されます。
カスタムイベントの場合、OnChartEvent関数のすべてのイベントをハンドルします。
そして再びMyFunction関数が呼ばれます。
MyFunction関数の周期的呼び出しはこの方法で実装され、
ミリセコンドの周期的呼び出をセットすることが出来ます。

スネークゲーム 10

Snake.mqhヘッダファイル

 Snake.mqhヘッダファイルにあるレベルマップは
int game_level[6][20][20]で表される3次元配列ですが、
左側の[6]はゲームのレベルになります。
[20][20]は、各レベルでのフィールドの初期状態を表しています。
エレメントの値は、
9が障害物、1がスネークの頭、2が胴体、3が尻尾を表します。

配列の数値代入を分りやすくするため、
3次元配列の各要素の数を2個で考えてみましょう。

1次元配列は、X[2]={a,b};となります。
2次元配列は、X[2][2]={{a,b},{c,d}};
1次元配列の{}をさらにネストして記述しています。
これは下のように書いても同じです。
X[2][2]=
{{a,b}
,{c,d}};
縦と横が揃っています。
(注:メール文では空白は無視されているので、コンマ「,」は先頭にしています。)


 3次元配列は、X[2][2][2]={{{a,b},{c,d}},{{e,f},{g,h}}};
これを2次元配列の時と同じようにすると、
以下のようになります。
X[2][2][2]=
{
{{a,b}
,{c,d}},
{{e,f}
,{g,h}}};
縦と横がきれいに揃っています。
(注:game_level[6][20][20]ではコンマと括弧の位置が違います。)

/> そろえることで、
ゲームのフィールドの状態が見やすくなります。
a~b、e~fは各フィールドの横
a~c、e~gは各フィールドの縦
として使うことで、スネークゲームと同じになります。
スネークゲームは[20][20]ですが、
同じようにすると、スネークの初期位置、
障害物の位置を簡単に把握出来るようになります。
数学的なアクロバットと言うよりも、物理的な2次元平面と言うところでしょうか。

 他に、Snake.mqhヘッダファイルでは、
SPEED_SNAKE
MAX_LENGTH
などゲームに必要な定数が定義されています。
SPEED_SNAKEはスネークの動作スピード、
MAX_LENGTHはスネーク胴体の最大長を定義しています。
(意味は、定義の後ろにコメントされています。)

 "Square" グラフィックラベルの名前の定義では、
#define SQUARE_BMP_LABEL_NAME    "snake_square_%u_%u"              
となっています。
BmpLabelを使うとき、
StringFormat (SQUARE_BMP_LABEL_NAME, 1,0)
と記述しますが、
これは "snake_square_1_0"と同じになります。
"snake_square_%u_%u"の%uは非負の整数を意味しています。
フィールドの各セルの名前になります。


スネークゲーム 11

ドーナッツ型フィールド(CChartFieldElementクラス)

CChartFieldElementクラスはCChartObjectBmpLabelクラスを継承しており、
プレイフィールド、障害物、スネーク、食べ物はこのクラスのオブジェクトとなります。
以下はクラスの詳細です。

private:
   int               pos_x,pos_y;

上記pos_x、pos_yはプレイフィールドのエレメントの相対的な位置を表します。

int               GetPosX(){return pos_x;}
int               GetPosY(){return pos_y;}

上記GetPosX、GetPosYは、pos_x、pos_yの値を取得します。

void              SetPos(int val_pos_x,int val_pos_y)
上記SetPosメソッドは、pos_x、pos_yの値をセットします。
SetPosメソッドの定義は以下のようになっています。

//A式
void              SetPos(int val_pos_x,int val_pos_y)
     {
      pos_x= (val_pos_x==-1)?COUNT_COLUMNS-1:((val_pos_x==COUNT_COLUMNS)?0:val_pos_x);
      pos_y= (val_pos_y==-1)?COUNT_ROWS-1:((val_pos_y==COUNT_ROWS)?0:val_pos_y);
//C式(擬似的クラインの壷の型)
   
     }

この式の中の
//B式
pos_x= (val_pos_x==-1)?COUNT_COLUMNS-1:((val_pos_x==COUNT_COLUMNS)?0:val_pos_x);

はネストされた三項演算子です。

pos_x= (val_pos_x==-1)?COUNT_COLUMNS-1:(P);
の式は、
val_pos_x==-1が、
trueならpos_x=COUNT_COLUMNS-1、
falseならpos_x=(P)の値になります。

(P)式は
val_pos_x==COUNT_COLUMNSがtrueの時は、pos_x=0。
val_pos_x==COUNT_COLUMNSがfalseの時は、pos_x=val_pos_xの値になります。


ここで、COUNT_COLUMNSの定義は ArrayRange(game_level,2) です。
したがって20です(プレイフィールドの列)。

val_pos_xの値を見てみましょう。
CSnakeGameクラスのメソッドから計算します。

1),CreateFieldメソッド

void CSnakeGame::CreateField()
{
CChartFieldElement *square_obj;
...
for(i=0;ifor(j=0;j{
...
square_obj.SetPos(j,i);
...
}
...

この式から、val_pos_x の値は、
0 <= val_pos_x < COUNT_COLUMNS=20であることが解ります。


2),CreateSnakeメソッド

void CSnakeGame::CreateSnake()
{
CChartFieldElement *snake_element_obj,*snake_arr[];
ArrayResize(snake_arr,3);
...
for(i=0;ifor(j=0;jif(game_level[current_level][i][j]==1 || game_level[current_level][i][j]==2 || game_level[current_level][i][j]==3)
{
...
snake_element_obj.SetPos(j,i);
...
}
...

このメソッドではプレイフィールドと同じように、
val_pos_x の値は、
0 <= val_pos_x < COUNT_COLUMNS=20です。

3),次はSnakeMoveOnFieldメソッドです。

void CSnakeGame::SnakeMoveOnField()
{
int prev_x,prev_y,next_x,next_y,check;
CChartFieldElement *snake_head_obj,*snake_body_obj,*snake_tail_obj;
...
prev_x=snake_head_obj.GetPosX();
...
switch(direction)
{
case DIRECTION_LEFT:snake_head_obj.SetPos(prev_x-1,prev_y);break;
case DIRECTION_UP:snake_head_obj.SetPos(prev_x,prev_y-1);break;
case DIRECTION_RIGHT:snake_head_obj.SetPos(prev_x+1,prev_y);break;
case DIRECTION_DOWN:snake_head_obj.SetPos(prev_x,prev_y+1);break;
}
...
}

snake_head_obj.SetPosの引数は
prev_x-1、prev_x+1
となっています。
prev_xが0の時、prev_x-1は-1、
19の時、prev_x+1は20です。
これはプレイフィールドの外側に出たことになります。

なのでA式から、
スネークはプレイフィールドの反対側に現れることになります。
pos_yも同様になります。
プレーイフィールドは、A式からトーラス(ドーナッツ)の形のフィールドになります。
スネークが左に進んで行くと右に現れ、
右に進んで行くと左に現れ、
上に進んで行くと下に現れ、
下に進んで行くと上に現れることになります。

このA式にさらに下記の2行を付け加えると、
擬似的クラインの壺の形のフィールドにすることが出来ます。
上に進んで行くと下に現れ、
下に進んで行くと上に現れるのは、トーラスの時と同じですが、
どんどん右に進んで行き左に現る時、行の位置が上下反転した位置になります。
左に進んで行き右に現る時も同じです。
//C式
if(val_pos_x==-1)pos_y=(COUNT_ROWS-val_pos_y);
else if(val_pos_x==COUNT_COLUMNS)pos_y=(COUNT_ROWS-val_pos_y);

本当のクラインの壺の形にするには表面と裏面が必要です。


次はCChartFieldElementクラスのMoveメソッドです。

void    Move(int start_pos_x,int start_pos_y)
{
X_Distance(start_pos_x+pos_x*SQUARE_WIDTH-pos_x+(SQUARE_WIDTH-X_Size())/2);
Y_Distance(start_pos_y+pos_y*SQUARE_HEIGHT-pos_y+(SQUARE_HEIGHT-Y_Size())/2);
}

Moveメソッドの中で使われているX_Distanceメソッド、Y_Distanceメソッドは、
親クラスのCChartObjectBmpLabelから呼び出されています。
MoveメソッドはX軸、Y軸の相対的距離をピクセル値に変換し、エレメントを移動します。
親クラスのCChartObjectBmpLabelクラスは、標準ライブラリクラスですので、すでにテスト済みです。
X_Distanceメソッドは、
ブラックボックスとして、インプットとアウトプットを考えて使うのが手っ取り早い方法です。
作った側は説明することまでを考えて作っていないらしく、入り組んだ構造になっています。
なので、標準ライブラリクラスのコードの説明は省略します。

代わりにプログラマーのコード作りのルールを少し紹介します。
管理者側のルールではなく、達人になるルールです。
これは「達人プログラマー」という本の中で紹介されているものです。
(著者:アンドリュー ハント、デビットトーマス)
1.自身の技術に関心を持つ事(長くやっていれば上達する)。
2.くり返しを避けること、システムの中のすべての知識は
重複なく、明瞭、信頼できる表現となっていなければならない。
(諸葛孔明の師匠である水鏡先生は、
弟子に対して「よし(好)」口ぐせだった。)
3.再利用しやすいようにしておくこと。
4.あとでビックリしないために、見積もりをおこなうこと。
5.パニックに陥らないこと(自動売買ばかりでなく、プログラム全般)。
6.設定すべきものを統合しないこと。
7.早めにテスト、何度もテスト、自動でテスト。
他にもありますが、筆者の好みの箇所を選びました。


---------------------------------------------------------------------------------
トーラス(torus、複数形:tori)、円環面、ドーナッツ

クラインの壺(くらいんのつぼ、英語: Klein bottle)は、
境界も表裏の区別も持たない(2次元)曲面の一種で、
主に位相幾何学で扱われる。
ユークリッド空間に埋め込むには4次元、曲率0とすると5次元が必要である。



void              Move(int start_pos_x,int start_pos_y)
     {
      X_Distance(start_pos_x+pos_x*SQUARE_WIDTH-pos_x+(SQUARE_WIDTH-X_Size())/2);
      Y_Distance(start_pos_y+pos_y*SQUARE_HEIGHT-pos_y+(SQUARE_HEIGHT-Y_Size())/2);
     }

----------------------------------------------------
class CChartObjectPanel : public CChartObjectButton

bool              X_Distance(const int X);

bool CChartObjectPanel::X_Distance(const int X)
  {
   CChartObjectLabel *chart_object;
//---
   for(int i=0;i     {
      chart_object=m_attachment.At(i);
      chart_object.X_Distance(X+m_dX.At(i));
     }
//---
   return(CChartObjectButton::X_Distance(X));

  }
-----------------------------------------------
class CChartObjectPanel : public CChartObjectButton
  {
protected:
   CArrayObj         m_attachment;       // array of attached objects
   CArrayInt         m_dX;               // array of dX attached objects

-----------------------------------------
bool CChartObjectPanel::Attach(CChartObjectLabel *chart_object)
  {
   if(m_attachment.Add(chart_object))//<-- p="">     {
      int x,y;
      x=chart_object.X_Distance();
      m_dX.Add(chart_object.X_Distance());//<-- p="">      x+=X_Distance();
      chart_object.X_Distance(X_Distance()+chart_object.X_Distance());
      y=CChartObjectButton::Y_Size();
      y+=chart_object.Y_Distance();
      m_dY.Add(chart_object.Y_Distance()+CChartObjectButton::Y_Size()+2);//<-- p="">      chart_object.Y_Distance(Y_Distance()+chart_object.Y_Distance()+CChartObjectButton::Y_Size()+2);
      return(true);
     }
//---
   return(false);
  }
--------------------------------------
----------------------------------------------
class CChartObjectLabel : public CChartObjectText
int               X_Distance(void) const;
bool              X_Distance(const int X) const;

int CChartObjectLabel::X_Distance(void) const
  {
//--- check
   if(m_chart_id==-1)
      return(0);
//--- result
   return((int)ObjectGetInteger(m_chart_id,m_name,OBJPROP_XDISTANCE));
  }
//+------------------------------------------------------------------+
//| Set the X-distance                                               |
//+------------------------------------------------------------------+
bool CChartObjectLabel::X_Distance(const int X) const
  {
//--- check
   if(m_chart_id==-1)
      return(false);
//--- result
   return(ObjectSetInteger(m_chart_id,m_name,OBJPROP_XDISTANCE,X));
  }

----------------------------------------------------
class CChartObjectButton : public CChartObjectEdit
  {
public:
                     CChartObjectButton(void);
                    ~CChartObjectButton(void);
   //--- methods of access to properties of the object
   bool              State(void) const;
   bool              State(const bool state) const;
   //--- method of identifying the object
   virtual int       Type(void) const { return(OBJ_BUTTON); }
   //--- methods for working with files
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
  };
--------------------------------------------------
class CChartObjectEdit : public CChartObjectLabel
 {
public:
                     CChartObjectEdit(void);
                    ~CChartObjectEdit(void);
   //--- methods of access to properties of the object
   bool              X_Size(const int X) const;
   bool              Y_Size(const int Y) const;
   color             BackColor(void) const;
   bool              BackColor(const color new_color) const;
   color             BorderColor(void) const;
   bool              BorderColor(const color new_color) const;
   bool              ReadOnly(void) const;
   bool              ReadOnly(const bool flag) const;
   ENUM_ALIGN_MODE   TextAlign(void) const;
   bool              TextAlign(const ENUM_ALIGN_MODE align) const;
   //--- change of angle is blocked
   bool              Angle(const double angle) const { return(false); }
   //--- method of creating the object
   bool              Create(long chart_id,const string name,const int window,const int X,const int Y,const int sizeX,const int sizeY);
   //--- method of identifying the object
   virtual int       Type(void) const { return(OBJ_EDIT); }
   //--- methods for working with files
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
  };
---------------------------------------------------
class CChartObjectLabel : public CChartObjectText
  {

   int               X_Distance(void) const;
   bool              X_Distance(const int X) const;

---------------------------------------------------
start_pos_x+pos_x*SQUARE_WIDTH-pos_x+(SQUARE_WIDTH-X_Size())/2


start_pos_x
pos_x
SQUARE_WIDTH
X_Size()
------------------------------------------------



スネークゲーム 12

CSnakeGameクラス

 CSnakeGameクラスはゲームのメインクラスです。
ゲーム要素の移動と除去、ゲーム生成のフィールドとメソッドを含んでいます。
クラスの宣言のとき、ゲーム要素ポインタの動的配列の組織化されたフィールドを見ることができます。
例えば、スネーク要素のポインタはsnake_element_obj_arr フィールドに保管されます。
snake_element_obj_arr 配列の0番目の配列インデックスはスネークの頭で、ラストは尻尾です。
プレイフィールドでの簡単な操作で、それを知ることが出来ます。

 CSnakeGameクラスのメソッドを考えて見ましょう。
メソッドはこのスネークゲームのセオリーの章で書かれた、セオリーの基本として実装されています。

The Snake
 衝突は、スネークの頭の移動の後、衝突のアイデンテイファイアを返すCheck() 関数によりチェックされます。
SetTrueSnake() 関数は、スネークの頭と、
その近傍要素の位置に従う尻尾の、正確な描写の記述に使用されます。

The Food for the Snake
 プレイフィールドの食べ物の位置がセルフィールドに提供され、
食べ物のその位置には他の要素は含まれません。

The Event Handling (final code)
 press_keyとpress_buttonは、2つともスタテイック変数です。
それらはOnChartEventイベントハンドラー関数で定義されます。
ゲームの開始は、press_button 変数がfalseのとき許可されます。
"Start"ボタンのクリックの後、press_button 変数がtrueにセットされます。
(それはスタートボタンによるコードの再実行を禁止します。)
この状態は下記のイベントの時まで同じ状態に保たれます。
現在のレベルの再スタート
次のレベルへの移行
ゲームがポウズ("Pause"ボタンがプレス)
ゲームがストップ("Stop"ボタンがプレス)

 現方向との方向が直角で、
プレイフィールドでのスネークの移動後(press_key変数の値)ならば、
スネークの移動方向の変更は可能です。
これらの状態は、
CHARTEVENT_KEYDOWNエベントプロセシング関数(keypressイベント)による、
アカウントから得られます。

 頭が動くと、CHARTEVENT_OBJECT_DRAGイベントが生成され、
CSnakeGameクラスのheader_leftとheader_top フィールドが再定義されます。
他のゲーム要素の動きは、これらフィールドの値で決められます。
プレイフィールドの動きは、
下記の 「TradePad_Sample」での方法を実装したものです。

TradePad_Sample - expert for MetaTrader 5
https://www.mql5.com/en/code/68