MQL5으로 "스네이크" 게임 만들기
들어가며
이 문서에서는 MQL5로 "스네이크" 게임을 만드는 예시를 살펴봅니다.
MQL의 다섯번째 버전에 커스텀 이벤트를 포함하는 이벤트 처리 기능들이 생기면서 게임 프로그래밍도 가능하게 되었습니다. MQL5는 객체 지향 프로그래밍이기에 프로그램의 설계를 단순화하고 코드를 명확하게 하며 오류 수를 줄여줍니다.
이 문서를 다 읽은 후에 당신은 OnChart 이벤트 처리, MQL5 표준 라이브러리 클래스와 일정 시간이 지날 때 마다 재차 계산을 하기 위한 순환 함수를 위한 레시피를 알게될 것입니다.
게임 설명
"스네이크" 게임을 고른 이유는 구현이 상당히 간단하기 때문입니다. 프로그래밍에 관심이 있는 사람이라면 누구건 쉽게 이 게임을 구현할 수 있죠.
위키피디아에 의하면:
스네이크는 1970년대 중반 오락실에서 처음 출시된 게임으로 그 이후에도 어느정도 인기를 유지하며 고전 게임으로 자리 잡았다고 합니다.
플레이어는 뱀처럼 길고 얇은 생명체를 조종하는데, 뱀은 움직일 수 있는 한계가 정해진 평면에서 먹이를 물고, 자신의 꼬리나 평면를 둘러싸고 있는 "벽"에 부딪히는 것을 피해야합니다. 몇몇 버전에서는 그 외에 장애물이 있기도 합니다. 뱀이 음식을 먹을 때마다 꼬리가 길어져서 게임은 몹시 어려워집니다. 사용자는 뱀의 머리 방향(위, 아래, 왼쪽 또는 오른쪽)을 조정하고 뱀의 몸은 그 방향으로 따라갑니다. 게임 진행 중에는 뱀의 움직임을 막을 수 없고, 반대로 가게 할 수도 없습니다.
MQL5으로 "스네이크"를 만들 때엔 몇가지 한계가 있습니다.
레벨의 수는 6개 (0에서 5). 각 레벨마다 목숨은 5개 있습니다. 목숨을 다 잃게되거나 모든 레벨을 성공적으로 돌파하게되면 첫 레벨로 돌아갑니다. 나만의 레벨을 만들 수 있습니다. 뱀의 속도와 최대 길이는 레벨에 상관없이 같습니다.
스네이크 게임의 필드에는 4가지 요소가 있습니다:
- 게임 타이틀. 이는 차트의 게임 위치 지정에 쓰입니다. 타이틀을 움직이면, 모든 게임 요소가 같이 이동됩니다.
- 플레이 필드. 이건 20x20 사이즈의 셀 어레이 (테이블) 입니다. 각 셀은 20x20 픽셀 사이즈로 되어있습니다. 플레이 필드의 요소들은:
- 뱀. 최소 3개의 요소로 구성되어있습니다 - 머리, 몸통, 꼬리. 머리는 위아래왼쪽오른쪽으로 움직일 수 있습니다. 뱀의 다른 요소들은 머리를 쫓아다닙니다.
- 장애물. 회색 직사각형으로 표현됩니다. 뱀의 머리가 장애물과 충돌하면 목숨이 하나 줄어들며 현재 레벨을 다시 시작합니다.
- 밥. 밥은 열매로 표현됩니다. 머리가 밥하고 닿게되면 뱀의 사이즈 (몸통의 길이) 가 증가합니다. 12조각을 먹은 뱀은 다음 단계로 올라갑니다.
- 정보 패널(게임의 상태 표시줄) 이 패널은 3개의 요소로 구성되어 있습니다:
- 레벨. 현재 레벨을 보여줍니다.
- 남은 음식. 남긴 음식의 갯수
- 목숨 수. 현재 남은 목숨의 수를 보여줍니다.
- 패널. 3개의 버튼으로 되어있습니다:
- "시작" 버튼. 현재 레벨을 시작합니다.
- "일시정지" 버튼. 게임을 잠시 멈춥니다.
- "그만하기" 버튼. 게임을 멈추고 첫 레벨로 돌아갑니다.
이 요소들은 전부 1번 그림에서 볼 수 있습니다:
1번 그림. "스네이크" 게임의 요소들
게임 제목은 "Button" 타입의 객체이며, 모든 필드 요소는 "BmpLabel" 타입의 객체입니다. 정보 패널은 "Edit" 타입의 객체 3개, 제어판은 "Button" 타입의 객체 3개로 구성되어 있습니다. 모든 개체는 차트의 왼쪽 상단 모서리에 상대적인 픽셀 단위의 X 및 Y를 따른 거리를 정의하여 배치됩니다.
운동장의 가장자리가 뱀의 움직임을 막지 않는다는 점에 주목해야 합니다. 예를 들어, 뱀이 왼쪽 가장자리를 통해 들어오면 오른쪽에 나타납니다. 이는 2번 그림으로 확인 가능합니다:
2번 그림. 필드 모서리를 통해 뱀이 들어오는 모습
몸과 달리 뱀의 머리와 꼬리는 회전할 수 있습니다. 머리 방향은 뱀의 움직임 방향 또는 주변 요소의 위치에 따라 결정됩니다. 꼬리의 방향은 인접 요소의 위치에 의해서만 결정됩니다.
예를 들어, 인접한 꼬리 요소가 왼쪽에 있으면 꼬리가 왼쪽으로 회전합니다. 머리는 조금 다릅니다. 만약 인접 요소가 오른쪽에 있으면 머리는 왼쪽으로 꺾입니다. 머리 방향과 꼬리 방향의 예는 아래 그림에 제시되어 있습니다. 주변 요소를 기준으로 머리와 꼬리를 회전시키도록 주의하십시오.
머리랑 꼬리는 왼쪽으로 꺾임 | 머리랑 꼬리는 오른쪽으로 꺾임 | 머리랑 꼬리는 아래로 꺾임 | 머리랑 꼬리는 위로 꺾임 |
뱀의 움직임은 3단계에 거쳐 이루어집니다:
- 하나의 셀 헤드가 방향에 따라 오른쪽, 왼쪽, 위쪽 또는 아래로 이동합니다.
- 뱀 몸체의 맨 뒤 요소는 기존 머리 위치에 따라 이동합니다.
- 뱀 꼬리는 마지막 몸통 요소의 위치를 따라갑니다. 뱀 꼬리는 마지막 몸통 요소의 위치를 따라갑니다.
만약 뱀이 음식을 먹으면 꼬리는 움직이지 않습니다. 대신 몸통에 새 요소가 생성되며 종전 마지막 요소가 있던 자리에 배치됩니다.
왼쪽의 뱀 움직임의 예가 아래 그림에 제시되어 있습니다.
초기 위치 | 머리 왼쪽으로 한 칸 | 마지막 몸통 요소의 움직임 머리의 기존 위치로 | 마지막 몸통 요소의 이전 위치로의 꼬리 움직임 |
이론
다음은 게임을 작성할 때 사용되는 도구와 기술에 대해 알아보겠습니다.
MQL5 표준 라이브러리
동일한 유형의 객체 어레이(예: 필드 셀, 스네이크 요소들)를 사용하여 객체(생성, 이동, 삭제)를 조작하는 것이 편리합니다. 이러한 어레이와 객체는 MQL5 표준 라이브러리 클래스를 사용하여 구현할 수 있습니다.
MQL5 표준 라이브러리 클래스들의 사용은 프로그램 제작 과정을 단순화시켜줍니다. 게임용으로 우리는 이하의 라이브러리 클래스들을 사용합니다:
- CArrayObj 는 데이터 조직 (포인터의 다이나믹 어레이) 용의 클래스입니다.
- CChartObjectEdit , CChartObjectButton , CChartObjectBmpLabel 들은 각각 "Edit", "Button" 그리고 "BmpLabel" 을 나타내는 컨트롤 클래스입니다.
MQL5 표준 라이브러리 클래스를 사용하려면 다음의 컴파일러 지침을 사용하여 포함시켜야합니다:
#include <path_to_the_file_with_classes_description>
예를들어 CChartObjectButton 타입 객체를 쓰려면 아래와 같이 써야하죠:
#include <ChartObjects\ChartObjectsTxtControls.mqh>
파일 경로는 MQL5 레퍼런스에서 찾을 수 있습니다.
MQL5 표준 라이브러리 클래스로 작업할 때는 이들 중 일부가 서로 상속된다는 점을 이해하는 것이 중요합니다. 예를 들면 CChartObjectButton 클래스는 CChartObjectEdit 클래스를 상속 받고, 한편 CChartObjectEdit 클래스는 CChatObjectLabel 클래스를 상속 받는 등 . 즉, 파생 클래스의 경우 상위 클래스 속성 및 메소드를 사용할 수 있습니다.
MQL5 표준 클래스 클래스의 사용 이점에 대해 이해하기 위해 버튼 생성의 예를 살펴보고 두 가지 방법으로(클래스를 사용하거나 사용하지 않음) 실행해 보겠습니다.
이것은 클래스를 사용하지 않는 예입니다.
//Creating a button with name "button" ObjectCreate(0,"button",OBJ_BUTTON,0,0,0); //Specifying the text on the button ObjectSetString(0,"button",OBJPROP_TEXT,"Button text"); //Specifying the button size ObjectSetInteger(0,"button",OBJPROP_XSIZE,100); ObjectSetInteger(0,"button",OBJPROP_YSIZE,20); //Specifying the button position ObjectSetInteger(0,"button",OBJPROP_XDISTANCE,10); ObjectSetInteger(0,"button",OBJPROP_YDISTANCE,10);
이것은 클래스를 사용한 예입니다
CChartObjectButton *button; //Creating an object of CChartObjectButton class and assign a pointer to the button variable button=new CChartObjectButton; //Creating a button with properties (in pixels): (width=100, height=20, positions: X=10,Y=10) button.Create(0,"button",0,10,10,100,20); //Specifying the text on the button button.Description("Button text");
보다시피 클래스를 쓰는 편이 간단합니다. 또한 클래스 객체를 어레이에 저장하고 쉽게 처리할 수 있습니다.
객체 관리 클래스의 방법과 속성은 표준 라이브러리 클래스에 대한 MQL5 레퍼런스 에 잘 설명되어 있습니다.
우리는 이제 표준 라이브러리 의 CArrayObj 클래스를 사용하여 객체 어레이를 정렬합니다. 이 클래스를 사용하면 새 요소를 추가할 때의 어레이 크기 조정, 어레이의 객체 삭제 등과 같은 많은 반복 작업을 수행할 필요가 없습니다.
CArrayObj 클래스의 기능들
CArrayObj 클래스를 사용하면 CObject 클래스 유형의 개체에 대한 동적 포인터 배열을 구성할 수 있습니다. CObject는 표준 라이브러리의 모든 클래스에 대한 상위 클래스입니다. 즉, 표준 라이브러리 클래스들의 객체 중 어떤 것에라도 동적 포인터 어레이를 생성할 수 있다는 의미입니다. 고유한 클래스의 동적 개체 배열을 생성해야 하는 경우 CObject 클래스에서 상속되어야 합니다.
다음 예에서는 커스텀 클래스가 CObject 클래스의 상속자 클래스이기 때문에 컴파일러에서 오류가 발생하지 않습니다.
#include <Arrays\ArrayObj.mqh> class CMyClass:public CObject { //fields and methods }; //creating an object of CMyClass type and assign it to the value of the my_obj variable CMyClass *my_obj=new CMyClass; //declaring a dynamic array of object pointers CArrayObj array_obj; //adding the my_obj object pointer at the end of the array_obj array array_obj.Add(my_obj);
그 다음 경우엔 my_obj가 CObject 클래스나 CObject 클래스를 상속하는 클래스에 대한 포인터가 아니기 때문에 컴파일러가 오류를 발생시킵니다.
#include <Arrays\ArrayObj.mqh> class CMyClass { //fields and methods }; //creating an object of CMyClass type and assing it to the value of the my_obj variable CMyClass *my_obj=new CMyClass; //declaring a dynamic array of object pointers CArrayObj array_obj; //adding the my_obj object pointer at the end of the array_obj array array_obj.Add(my_obj);
스네이크 게임을 짤 때엔 CArrayObj 클래스의 메소드들을 이용합니다:
- Add - 어레이 끝에 요소를 추가함.
- Insert - 어레이의 특정 위치에 요소를 삽입함.
- Detach - 어레이 내 특정 위치의 요소를 제거함 (어레이에서 해당 요소는 삭제됨).
- Total - 어레이 내 요소의 숫자를 리턴함.
- At - 어레이 내 특정 요소의 위치를 받아옴 (어레이에서 해당 요소는 삭제되지않음).
CArrayObj 클래스를 다루는 예시입니다:
#include <Arrays\ArrayObj.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CMyClass:public CObject { public: char s; }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void MyPrint(CArrayObj *array_obj) { CMyClass *my_obj; for(int i=0;i<array_obj.Total();i++) { my_obj=array_obj.At(i); printf("%C",my_obj.s); } } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //creating the pointer to the object of CArrayObj class CArrayObj *array_obj=new CArrayObj(); //declaring the CMyClass object pointer CMyClass *my_obj; //filling the array_obj dynamic array for(int i='a';i<='c';i++) { //creating the CMyClass class object my_obj=new CMyClass(); my_obj.s=char(i); //adding an object of CMyClass class at the end of the array_obj dynamic array array_obj.Add(my_obj); } //printing result MyPrint(array_obj); //creating new object of CMyClass class my_obj=new CMyClass(); my_obj.s='d'; //inserting new element at the first position of the array array_obj.Insert(my_obj,1); //printing result MyPrint(array_obj); //detaching the element from the third position of the array my_obj=array_obj.Detach(2); //printing result MyPrint(array_obj); //deleting the dynamic array and all objects with pointers of the array delete array_obj; return(0); }
본 예시에서 OnInit 함수는 3개의 요소가 들어있는 동적 어레이를 생성합니다. 어레이 컨텐츠의 출력은 MyPrint 기능을 호출하여 수행됩니다.
Add 메소드를 사용해 어레이를 채운 후 해당 어레이의 컨텐츠는 (a,b,c)로 표현될 수 있습니다.
Insert 메소드를 적용시킨 후 해당 어레이의 컨텐츠는 (a, d, b, c)로 표현될 수 있습니다.
마지막으로 Detach 메소드를 적용시킨 후 해당 어레이의 컨텐츠는 (a, d, c) 처럼 보일 것입니다.
delete 연산자가 array_obj 변수에 적용되었을 땐 CArrayObj 클래스의 소멸자가 호출되어 array_obj 어레이를 제거할 뿐만 아니라 포인터들이 저장된 객체들도 한번에 날려버립니다. 이를 방지하기 위해 delete 명령어를 사용하기 전에 CArrayObj 클래스의 메모리 관리 플래그가 false로 세팅되어야합니다. 이 플래그는 FreeMode 메소드를 통해 세팅됩니다.
만약 객체 포인터의 동적 어레이를 지울 때에 그 동적 어레이에 저장된 포인터에 대응되는 객체들을 삭제할 필요가 없을 경우엔 이하와 같이 코드를 쓰면 됩니다:
array_obj.FreeMode(false);
delete array_obj;
이벤트 관리
생성된 이벤트 세트가 있으면 해당 이벤트가 대기열에 누적된 다음 이벤트 처리 기능에 일관되게 도착합니다.
차트 작업 시에 생성되는 이벤트 핸들링 및 커스텀 이벤트를 위해서, MQL5에는 OnChartEvent 함수가 있습니다. 각각의 이벤트에서 OnChartEvent 함수로 식별자와 패러미터가 전달됩니다.
OnChartEvent 함수는 스레드가 다른 모든 프로그램 함수에서 빠져나왔을 때 호출됩니다. 따라서 이하의 예시에서는 OnChartEvent는 권한을 쥐지 못합니다.
#include <ChartObjects\ChartObjectsTxtControls.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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 } } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MyFunction(); return(0); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id==CHARTEVENT_OBJECT_CLICK && sparam=="button") Alert("Button click"); }
무한으로 도는 while 루프는 MyFunction 함수에서 값이 리턴되는걸 허락하지 않습니다. OnChartEvent 이 통제할 수가 없죠. 따라서 이 버튼을 눌러도 Alert 함수가 불리지 않습니다.
이벤트 처리를 통한 주기적 코드 실행
게임에서는 일정 시간 간격 후 이벤트 처리 함수와 함께 뱀 움직임을 위해 주기적인 호출이 필요합니다. 하지만 위에서 설명한 것처럼 무한 루프는 OnChartEvent 함수를 호출하지 않으니 이벤트를 처리할 수 없게 됩니다.
따라서 주기적인 코드 실행을 위한 또 다른 방법을 만들어내야 합니다.
OnTimer 사용하기
MQL5 언어에는 미리 설정해둔 시간(초)에 따라 주기적으로 호출되는 특별한 OnTimer 기능이 있습니다. 그렇게 하기 위해 EventSetTimer 함수를 사용할 것입니다.
이전 예시는 이렇게 다시 쓰일 수 있습니다:
#include <ChartObjects\ChartObjectsTxtControls.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void MyFunction() { //The code, that should be executed periodically } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ 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 && sparam=="button") Alert("Button click"); }
OnInit 함수에서 그 버튼이 1초에 해당되는 기간을 OnTimer 함수 호출을 위해 생성 후 정의합니다. OnTimer 함수는 매 초 호출되며, OnTimer 함수가 주기적으로 호출되어야하는 코드 (MyFunction) 를 호출합니다.
OnTimer 함수의 호출 시간은 몇 초정도인 것에 주목하십시오. 지정된 시간(밀리초) 후에 함수를 호출하려면 다른 방법이 필요합니다. 이 방식은 커스텀 이벤트를 쓰는 것입니다.
커스텀 이벤트 사용하기
커스텀 이벤트는 EventChartCustom 함수로 생성되며, 이벤트 ID 및 패러미터들은 EventChartCustom 함수의 입력 패러미터에 정의되어있습니다. 사용자 정의 ID 수는 최대 65536개(0 ~ 65535개)일 수 있습니다. MQL5 컴파일러는 사용자 지정 이벤트를 다른 유형의 이벤트와 구별하기 위해 CHARTEVENT_CUSTOM 상수 식별자를 ID에 자동으로 추가합니다. 커스텀 ID의 실제 범위는 CHARTEVENT_CUSTOM에서 CHARTEVENT_CUSTOM+65535 ( CHARTEVENT_CUSTOM_LAST ).
맞춤형 이벤트를 사용하는 MyFunction의 주기적 호출 예는 다음과 같습니다.
#include <ChartObjects\ChartObjectsTxtControls.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void MyFunction() { //The code, that should be executed periodically Sleep(200); EventChartCustom(0,0,0,0,""); } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { CChartObjectButton *button; button=new CChartObjectButton; button.Create(0,"button",0,10,10,100,20); button.Description("Button text"); MyFunction(); return(0); } //+------------------------------------------------------------------+ //| OnChartEvent processing function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id==CHARTEVENT_OBJECT_CLICK && sparam=="button") Alert("Button click"); if(id==CHARTEVENT_CUSTOM) MyFunction(); }
이 예에서, My Function 함수 이전에 200 ms(주기적 호출 시간)의 지연이 있고 커스텀 이벤트가 생성됩니다. OnChartEvent 함수가 모든 이벤트를 관리하며, 커스텀 이벤트의 경우엔 MyFunction 하함수를 다시 호출합니다. 따라서, MyFunction 함수의 주기적인 호출는 이러한 방식으로 구현되며, 통화 기간을 밀리초로 설정할 수 있습니다.
실전 파트
"스네이크" 게임을 짜는 예시를 고려해봅시다.
상수값 정의 및 레벨 맵
맵 레벨은 별도의 헤더 파일인 "Snake.mqh"에 들어있으며 3차원 어레이 level [6] [20] [20] 입니다. 레벨 맵은 별도의 헤더 파일인 "Snake.mqh"에 들어 있으며 3차원 어레이 game_level[6][20][20] 입니다. 이 어레이의 각 요소는 개별 수준에 대한 설명을 포함하는 2차원 어레이입니다. 요소의 값이 만약 9면 이는 장애물입니다. 어레이 요소의 값이 1,2 또는 3이면 뱀의 머리, 몸통 또는 꼬리의 각각 게임 필드에서의 초기 위치를 정의합니다. 새 레벨을 추가하거나 기존 레벨을 레벨 어레이에서 수정할 수 있습니다.
덤으로 "Snake.mqh" 파일에는 우리가 게임에서 쓸 상수 역시 담겨있습니다. 예를 들어 SPEED_SNAKE 및 MAX_LENGE_SNAKE 콘텐트를 변경하여 각 수준에서 뱀의 속도와 최대 길이를 늘리거나 줄일 수 있습니다. 모든 상수엔 코멘트가 달려있습니다.
//+------------------------------------------------------------------+ //| Snake.mqh | //| Copyright Roman Martynyuk | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Roman Martynyuk" #property link "http://www.mql5.com" #include <VirtualKeys.mqh> //File with keycodes #include <Arrays\ArrayObj.mqh> //File with CArrayObj class #include <ChartObjects\ChartObjectsBmpControls.mqh> //File with CChartObjectBmpLabel class #include <ChartObjects\ChartObjectsTxtControls.mqh> //File with CChartObjectButton and CChartObjectEdit classes #define CRASH_NO 0 //No crash #define CRASH_OBSTACLE_OR_SNAKE 1 //Crash with an "Obstacle" or snake body #define CRASH_FOOD 2 //Crash with a "Food"" #define DIRECTION_LEFT 0 //Left #define DIRECTION_UP 1 //Up #define DIRECTION_RIGHT 2 //Right #define DIRECTION_DOWN 3 //Down #define COUNT_COLUMNS ArrayRange(game_level,2) //Number of columns of playing field #define COUNT_ROWS ArrayRange(game_level,1) //Number of rows of playing field #define COUNT_LEVELS ArrayRange(game_level,0) //Number of levels #define START_POS_X 0 //Starting X position of the game #define START_POS_Y 0 //Starting Y position of the game #define SQUARE_WIDTH 20 //Square (cell) width (in pixels) #define SQUARE_HEIGHT 20 //Square (cell) height (in pixels) #define IMG_FILE_NAME_SQUARE "\\Images\\Games\\Snake\\square.bmp" //Path to the "Square" image #define IMG_FILE_NAME_OBSTACLE "\\Images\\Games\\Snake\\obstacle.bmp" //Path to the "Obstacle" image #define IMG_FILE_NAME_SNAKE_HEAD_LEFT "\\Images\\Games\\Snake\\head_left.bmp" //Path to the snake's head (left) image #define IMG_FILE_NAME_SNAKE_HEAD_UP "\\Images\\Games\\Snake\\head_up.bmp" //Path to the snake's head (up) image #define IMG_FILE_NAME_SNAKE_HEAD_RIGHT "\\Images\\Games\\Snake\\head_right.bmp" //Path to the snake's head (right) image #define IMG_FILE_NAME_SNAKE_HEAD_DOWN "\\Images\\Games\\Snake\\head_down.bmp" //Path to the snake's head (down) image #define IMG_FILE_NAME_SNAKE_BODY "\\Images\\Games\\Snake\\body.bmp" //Path to the snake's body image #define IMG_FILE_NAME_SNAKE_TAIL_LEFT "\\Images\\Games\\Snake\\tail_left.bmp" //Path to the snake's tail (left) image #define IMG_FILE_NAME_SNAKE_TAIL_UP "\\Images\\Games\\Snake\\tail_up.bmp" //Path to the snake's tail (up) image #define IMG_FILE_NAME_SNAKE_TAIL_RIGHT "\\Images\\Games\\Snake\\tail_right.bmp" //Path to the snake's tail (right) image #define IMG_FILE_NAME_SNAKE_TAIL_DOWN "Games\\Snake\\tail_down.bmp" //Path to the snake's tail (down) image #define IMG_FILE_NAME_FOOD "Games\\Snake\food.bmp" //Path to the "Food" image #define SQUARE_BMP_LABEL_NAME "snake_square_%u_%u" //Name of the "Square" graphic label #define OBSTACLE_BMP_LABEL_NAME "snake_obstacle_%u_%u" //Name of the "Obstacle" graphic label #define SNAKE_ELEMENT_BMP_LABEL_NAME "snake_element_%u" //Name of the "Snake" graphic label #define FOOD_BMP_LABEL_NAME "snake_food_%u" //Name of the "Food" graphic label #define LEVEL_EDIT_NAME "snake_level_edit" //Name of the "Level" edit #define LEVEL_EDIT_TEXT "Level: %u of %u" //Text of the "Level" edit #define FOOD_LEFT_OVER_EDIT_NAME "snake_food_available_edit" //Name of the "Food left" edit #define FOOD_LEFT_OVER_EDIT_TEXT "Food left over: %u" //Text of the "Food left" edit #define LIVES_EDIT_NAME "snake_lives_edit" //Name of the "Lives" edit #define LIVES_EDIT_TEXT "Lives: %u" //Text of the "Lives" edit #define START_GAME_BUTTON_NAME "snake_start_game_button" //Name of the "Start" button #define START_GAME_BUTTON_TEXT "Start" //Text of the "Start" button #define PAUSE_GAME_BUTTON_NAME "snake_pause_game_button" //Name of the "Pause" button #define PAUSE_GAME_BUTTON_TEXT "Pause" //Text of the "Pause" button #define STOP_GAME_BUTTON_NAME "snake_stop_game_button" //Name of the "Stop" button #define STOP_GAME_BUTTON_TEXT "Stop" //Text of the "Stop" button #define CONTROL_WIDTH (COUNT_COLUMNS*(SQUARE_WIDTH-1)+1)/3//Control Panel Width (1/3 of playing field width) #define CONTROL_HEIGHT 40 //Control Panel Height #define CONTROL_BACKGROUND C'240,240,240' //Color of Control Panel buttons #define CONTROL_COLOR Black //Text Color of Control Panel Buttons #define STATUS_WIDTH (COUNT_COLUMNS*(SQUARE_WIDTH-1)+1)/3//Status Panel Width (1/3 of playing field width) #define STATUS_HEIGHT 40 //Status Panel Height #define STATUS_BACKGROUND LemonChiffon //Status Panel Background Color #define STATUS_COLOR Black //Status Panel Text Color #define HEADER_BUTTON_NAME "snake_header_button" //Name of the "Header" button #define HEADER_BUTTON_TEXT "Snake" //Text of the "Header" button #define HEADER_WIDTH COUNT_COLUMNS*(SQUARE_WIDTH-1)+1 //Width of the "Header" button (playing field width) #define HEADER_HEIGHT 40 //Height of the "Header" button #define HEADER_BACKGROUND BurlyWood //Header Background Color #define HEADER_COLOR Black //Headet Text Color #define COUNT_FOOD 3 //Number of "Foods" at playing field #define LIVES_SNAKE 5 //Number of snake lives at each level #define SPEED_SNAKE 100 //Snake Speed (in milliseconds) #define MAX_LENGTH_SNAKE 15 //Maximal Snake Length #define MAX_LEVEL COUNT_LEVELS-1 //Maximal Level int game_level[][20][20]= { { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,3,2,1,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} } , { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,9,9,9,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} } , { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,9,1,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,9,9,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,9,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} } , { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0}, {0,0,0,0,0,9,9,0,0,0,0,0,3,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,9,9,9,9,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} } , { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,1,2,3,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,9,9,9,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,9,9,9,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,9,9,9,9,0}, {0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,9,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} } , { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0}, {0,1,2,3,0,0,0,0,0,0,0,9,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,9,9,9,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,9,9,0,0,0,0,0,0,9,9,9,9,0}, {0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,9,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0} } }; //+------------------------------------------------------------------+
상수 #define의 정의 SQUARE_BMP_LABEL_NAME "snake_square_% u_% U". 이제 필드를 만들겁니다. 필드의 각 셀은 비트맵 레이블이며 고유한 이름을 가져야 합니다. 셀의 이름은 이 상수로 정의되고, 셀 이름의 포맷 설정은 %u이며, unsigned integer를 의미합니다.
BmpLabel을 생성할 때 다음과 같이 이름을 지정할 경우: StringFormat(SQUARE_BMP_LABEL_NAME, 1.0)의 이름은 "snake_square_1_0"입니다.
클래스
게임용으로 개발된 커스텀 클래스가 2개 있는데 모두 "Snake.mq5" 파일에 있습니다.
ChartFieldElement 클래스:
//+------------------------------------------------------------------+ //| CChartFieldElement class | //+------------------------------------------------------------------+ class CChartFieldElement:public CChartObjectBmpLabel { private: int pos_x,pos_y; public: int GetPosX(){return pos_x;} int GetPosY(){return pos_y;} //setting position (pos_x,pos_y) in internal coordinates 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); } //conversion of internal coordinates to absolute and object movement on the chart 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); } };
CChartFiledElement 클래스는 CChartObjectBmpLabel 클래스를 상속받아 연장합니다. 필드, 머리, 몸통, 꼬리, 그리고 "음식"이 이 클래스의 객체입니다 pos_x 및 pos_y 속성은 필드 내에서 해당 요소의 상대적 수평 및 수직 좌표입니다. SetPos 메소드가 이들 좌표를 설정합니다. Move 메소드는 상대 좌표를 픽셀 단위로 X 및 Y 축을 따라 있는 거리로 변환하고 요소를 이동합니다. 그를 위해 Move 메소드는 CChartObjectBmpLabel 클래스의 X_Distance 및 YDistance 메소드를 호출합니다.
CSnakeGame 클래스:
//+------------------------------------------------------------------+ //| CSnakeGame class | //+------------------------------------------------------------------+ class CSnakeGame { private: CArrayObj *square_obj_arr; //Array of playing field cells CArrayObj *control_panel_obj_arr; //Array of control panel buttons CArrayObj *status_panel_obj_arr; //Array of control panel edits CArrayObj *obstacle_obj_arr; //Array of an obstacles CArrayObj *food_obj_arr; //Array of "Food" CArrayObj *snake_element_obj_arr; //Array of snake elements CChartObjectButton *header; //Header int direction; //Snake movement direction int current_lives; //Number of snake Lives int current_level; //Level int header_left; //Left position of a header (X) int header_top; //Top position of a header (Y) public: //class constructor void CSnakeGame() { current_lives=LIVES_SNAKE; current_level=0; header_left=START_POS_X; header_top=START_POS_Y; } //method for definition of header_left and header_top fields void SetHeaderPos(int val_header_left,int val_header_top) { header_left=val_header_left; header_top=val_header_top; }; //Get/Set direction methods void SetDirection(int d){direction=d;} int GetDirection(){return direction;} //Header creation and deletion methods void CreateHeader(); void DeleteHeader(); //Playing field creation, movement and deletion methods void CreateField(); void FieldMoveOnChart(); void DeleteField(); //Obstacle creation, movement and deletion methods void CreateObstacle(); void ObstacleMoveOnChart(); void DeleteObstacle(); //Snake creation, movement and deletion methods void CreateSnake(); void SnakeMoveOnChart(); void SnakeMoveOnField(); //snake movement on the playing field void SetTrueSnake(); //setting the images of the current snake's head and tail int Check(); //check for the collision with the playing field elements void DeleteSnake(); //Food creation, movement and deletion methods void CreateFood(); void FoodMoveOnChart(); void FoodMoveOnField(int food_num); void DeleteFood(); //Status panel creation, movement and deletion methods void CreateControlPanel(); void ControlPanelMoveOnChart(); void DeleteControlPanel(); //Control panel creation, movement and deletion methods void CreateStatusPanel(); void StatusPanelMoveOnChart(); void DeleteStatusPanel(); //Move all elements on the chart void AllMoveOnChart(); //Game initialization void Init(); //Game deinitialization void Deinit(); //Game control methods void StartGame(); void PauseGame(); void StopGame(); void ResetGame(); void NextLevel(); };
CSnakeGame은 이 게임의 메인 클래스이며 필드와 게임 요소를 생성, 이동 및 제거하는 메소드를 포함하고 있습니다. 클래스 설명을 시작할 때 게임 요소의 동적 어레이 구성을 위한 필드가 선언된 것을 볼 수 있습니다. 예를 들어 snake 요소의 포인터는 snake_element_obj_arr 필드에 저장됩니다. snake_element_obj_arr 어레이의 0번째 인덱스는 뱀의 머리이자 마지막 - 꼬리입니다. 그걸 알면 필드 상의 뱀을 쉽게 조작할 수 있습니다.
CSnakeGame 클래스의 메소드들을 다시 봅시다. 이 메소드들은 이 문서의 "이론" 챕터에 제시된 이론을 기반으로 구현됩니다.
게임 헤더
//+------------------------------------------------------------------+ //| Header creation method | //+------------------------------------------------------------------+ void CSnakeGame::CreateHeader(void) { //creating a new object of CChartObjectButton class and specifying the properties of header of CSnakeGame class header=new CChartObjectButton; header.Create(0,HEADER_BUTTON_NAME,0,header_left,header_top,HEADER_WIDTH,HEADER_HEIGHT); header.BackColor(HEADER_BACKGROUND); header.Color(HEADER_COLOR); header.Description(HEADER_BUTTON_TEXT); //the header is selectable header.Selectable(true); } //+------------------------------------------------------------------+ //| Header deletion method | //+------------------------------------------------------------------+ void CSnakeGame::DeleteHeader(void) { delete header; }
게임 필드
//+------------------------------------------------------------------+ //| Playing Field creation method | //+------------------------------------------------------------------+ void CSnakeGame::CreateField() { int i,j; CChartFieldElement *square_obj; //creating an object of CArrayObj class and assign the square_obj_arr properties of CSnakeGame class square_obj_arr=new CArrayObj(); for(i=0;i<COUNT_ROWS;i++) for(j=0;j<COUNT_COLUMNS;j++) { square_obj=new CChartFieldElement(); square_obj.Create(0,StringFormat(SQUARE_BMP_LABEL_NAME,i,j),0,0,0); square_obj.BmpFileOn(IMG_FILE_NAME_SQUARE); //specifying the internal coordinates of the cell square_obj.SetPos(j,i); square_obj_arr.Add(square_obj); } //moving the playing field cells FieldMoveOnChart(); ChartRedraw(); } //+------------------------------------------------------------------+ //| The movement of playing field cells on the chart | //+------------------------------------------------------------------+ void CSnakeGame::FieldMoveOnChart() { CChartFieldElement *square_obj; int i; i=0; while((square_obj=square_obj_arr.At(i))!=NULL) { square_obj.Move(header_left,header_top+HEADER_HEIGHT); i++; } ChartRedraw(); } //+------------------------------------------------------------------+ //| Deletion of a playing field | //+------------------------------------------------------------------+ void CSnakeGame::DeleteField() { delete square_obj_arr; ChartRedraw(); }
장애물
//+------------------------------------------------------------------+ //| Creation of the obstacles | //+------------------------------------------------------------------+ void CSnakeGame::CreateObstacle() { int i,j; CChartFieldElement *obstacle_obj; //creating an object of CArrayObj class and assign the obstacle_obj_arr properties of CSnakeGame class obstacle_obj_arr=new CArrayObj(); for(i=0;i<COUNT_ROWS;i++) for(j=0;j<COUNT_COLUMNS;j++) if(game_level[current_level][i][j]==9) { obstacle_obj=new CChartFieldElement(); obstacle_obj.Create(0,StringFormat(OBSTACLE_BMP_LABEL_NAME,i,j),0,0,0); obstacle_obj.BmpFileOn(IMG_FILE_NAME_OBSTACLE); //specifying the internal coordinates of the obstacle obstacle_obj.SetPos(j,i); obstacle_obj_arr.Add(obstacle_obj); } //moving the obstacle on the chart ObstacleMoveOnChart(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Obstacle movement method | //+------------------------------------------------------------------+ void CSnakeGame::ObstacleMoveOnChart() { CChartFieldElement *obstacle_obj; int i; i=0; while((obstacle_obj=obstacle_obj_arr.At(i))!=NULL) { obstacle_obj.Move(header_left,header_top+HEADER_HEIGHT); i++; } ChartRedraw(); } //+------------------------------------------------------------------+ //| Obstacle deletion method | //+------------------------------------------------------------------+ void CSnakeGame::DeleteObstacle() { delete obstacle_obj_arr; ChartRedraw(); }
뱀
//+------------------------------------------------------------------+ //| Snake creation method | //+------------------------------------------------------------------+ void CSnakeGame::CreateSnake() { int i,j; CChartFieldElement *snake_element_obj,*snake_arr[]; ArrayResize(snake_arr,3); //creating an object of CArrayObj class and assign it to the snake_element_obj_arr properties of CSnakeGame class snake_element_obj_arr=new CArrayObj(); for(i=0;i<COUNT_COLUMNS;i++) for(j=0;j<COUNT_ROWS;j++) if(game_level[current_level][i][j]==1 || game_level[current_level][i][j]==2 || game_level[current_level][i][j]==3) { snake_element_obj=new CChartFieldElement(); snake_element_obj.Create(0,StringFormat(SNAKE_ELEMENT_BMP_LABEL_NAME,game_level[current_level][i][j]),0,0,0); snake_element_obj.BmpFileOn(IMG_FILE_NAME_SNAKE_BODY); //specifying the internal coordinates of the snake element snake_element_obj.SetPos(j,i); snake_arr[game_level[current_level][i][j]-1]=snake_element_obj; } snake_element_obj_arr.Add(snake_arr[0]); snake_element_obj_arr.Add(snake_arr[1]); snake_element_obj_arr.Add(snake_arr[2]); //moving the snake on the chart SnakeMoveOnChart(); //setting the correct images of the snake's head and tail SetTrueSnake(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Snake movement on the chart | //+------------------------------------------------------------------+ void CSnakeGame::SnakeMoveOnChart() { CChartFieldElement *snake_element_obj; int i; i=0; while((snake_element_obj=snake_element_obj_arr.At(i))!=NULL) { snake_element_obj.Move(header_left,header_top+HEADER_HEIGHT); i++; } ChartRedraw(); } //+------------------------------------------------------------------+ //| Snake movement on the playing field | //+------------------------------------------------------------------+ void CSnakeGame::SnakeMoveOnField() { int prev_x,prev_y,next_x,next_y,check; CChartFieldElement *snake_head_obj,*snake_body_obj,*snake_tail_obj; //getting the snake's head from the array snake_head_obj=snake_element_obj_arr.At(0); //saving the coordinates of a head prev_x=snake_head_obj.GetPosX(); prev_y=snake_head_obj.GetPosY(); //setting the new internal coordinates for the head depending on the movement direction 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; } //moving the snake's head snake_head_obj.Move(header_left,header_top+HEADER_HEIGHT); //check for the snake's head collision with the other playing field elements (obstacle, snake body, food) check=Check(); //getting the last element of the snake's body snake_body_obj=snake_element_obj_arr.Detach(snake_element_obj_arr.Total()-2); //saving coordinates of the snake's body next_x=snake_body_obj.GetPosX(); next_y=snake_body_obj.GetPosY(); //moving the snake's body to the previous head's position snake_body_obj.SetPos(prev_x,prev_y); snake_body_obj.Move(header_left,header_top+HEADER_HEIGHT); //saving the previous position of the snake's body prev_x=next_x; prev_y=next_y; //inserting the snake's body to the first position of the snake_element_obj_arr array snake_element_obj_arr.Insert(snake_body_obj,1); //if the snake's head has collided with the "Food" if(check>=CRASH_FOOD) { //creating new element of the snake's body snake_body_obj=new CChartFieldElement(); snake_body_obj.Create(0,StringFormat(SNAKE_ELEMENT_BMP_LABEL_NAME,snake_element_obj_arr.Total()+1),0,0,0); snake_body_obj.BmpFileOn(IMG_FILE_NAME_SNAKE_BODY); //moving the body element to the end of the snake before the tail snake_body_obj.SetPos(prev_x,prev_y); snake_body_obj.Move(header_left,header_top+HEADER_HEIGHT); //adding the body to the penultimate position of the snake_element_obj_arr array snake_element_obj_arr.Insert(snake_body_obj,snake_element_obj_arr.Total()-1); //if snake's body isn't equal to the maximal snake length if(snake_element_obj_arr.Total()!=MAX_LENGTH_SNAKE) { //moving the eaten element on the new place on the playing field FoodMoveOnField(check-CRASH_FOOD); } //else we generate the custom event, that indicates that current snake length is the maximal possible else EventChartCustom(0,2,0,0,""); } //else if there isn't collision with the food, moving the tail to the position of the snake's body else { snake_tail_obj=snake_element_obj_arr.At(snake_element_obj_arr.Total()-1); snake_tail_obj.SetPos(prev_x,prev_y); snake_tail_obj.Move(header_left,header_top+HEADER_HEIGHT); } //setting the correct images for the head and tail SetTrueSnake(); ChartRedraw(); //generating the custom event for periodic call of this snake movement function EventChartCustom(0,0,0,0,""); Sleep(SPEED_SNAKE); } //+------------------------------------------------------------------+ //| Setting the correct images for the snake's head and tail | //+------------------------------------------------------------------+ void CSnakeGame::SetTrueSnake() { CChartFieldElement *snake_head,*snake_body,*snake_tail; int total,x1,x2,y1,y2; total=snake_element_obj_arr.Total(); //getting the snake's head snake_head=snake_element_obj_arr.At(0); //saving position of a head x1=snake_head.GetPosX(); y1=snake_head.GetPosY(); //getting the first element of the snake's body snake_body=snake_element_obj_arr.At(1); //saving coordinates of the body x2=snake_body.GetPosX(); y2=snake_body.GetPosY(); //choosing the file with an image depening on the position of the head and the first body element relative to each other //setting the snake's movement direction depending on the snake's head direction if(x1-x2==1 || x1-x2==-(COUNT_COLUMNS-1)) { snake_head.BmpFileOn(IMG_FILE_NAME_SNAKE_HEAD_RIGHT); direction=DIRECTION_RIGHT; } else if(y1-y2==1 || y1-y2==-(COUNT_ROWS-1)) { snake_head.BmpFileOn(IMG_FILE_NAME_SNAKE_HEAD_DOWN); direction=DIRECTION_DOWN; } else if(x1-x2==-1 || x1-x2==COUNT_COLUMNS-1) { snake_head.BmpFileOn(IMG_FILE_NAME_SNAKE_HEAD_LEFT); direction=DIRECTION_LEFT; } else { snake_head.BmpFileOn(IMG_FILE_NAME_SNAKE_HEAD_UP); direction=DIRECTION_UP; } //getting the last element of the snake's body snake_body=snake_element_obj_arr.At(total-2); //saving coordinates of the body x1=snake_body.GetPosX(); y1=snake_body.GetPosY(); //getting the tail of the snake snake_tail=snake_element_obj_arr.At(total-1); //saving coordinates of the tail x2=snake_tail.GetPosX(); y2=snake_tail.GetPosY(); //choosing the file with an image depening on the position of the tail and the last body element relative to each other if(x1-x2==1 || x1-x2==-(COUNT_COLUMNS-1)) snake_tail.BmpFileOn(IMG_FILE_NAME_SNAKE_TAIL_RIGHT); else if(y1-y2==1 || y1-y2==-(COUNT_ROWS-1)) snake_tail.BmpFileOn(IMG_FILE_NAME_SNAKE_TAIL_DOWN); else if(x1-x2==-1 || x1-x2==COUNT_COLUMNS-1) snake_tail.BmpFileOn(IMG_FILE_NAME_SNAKE_TAIL_LEFT); else snake_tail.BmpFileOn(IMG_FILE_NAME_SNAKE_TAIL_UP); } //+------------------------------------------------------------------+ //| Check for snake's head collision with the playing field elements | //+------------------------------------------------------------------+ int CSnakeGame::Check() { int i; CChartFieldElement *snake_head_obj,*snake_element_obj,*obstacle_obj,*food_obj; //getting the snake's head snake_head_obj=snake_element_obj_arr.At(0); i=0; //check for the head's collision with the obstacle while((obstacle_obj=obstacle_obj_arr.At(i))!=NULL) { if(snake_head_obj.GetPosX()==obstacle_obj.GetPosX() && snake_head_obj.GetPosY()==obstacle_obj.GetPosY()) { EventChartCustom(0,1,0,0,""); return CRASH_OBSTACLE_OR_SNAKE; } i++; } i=0; //check for the collision of head with the food while((food_obj=food_obj_arr.At(i))!=NULL) { if(snake_head_obj.GetPosX()==food_obj.GetPosX() && snake_head_obj.GetPosY()==food_obj.GetPosY()) { //hiding the food food_obj.Background(true); return(CRASH_FOOD+i); } i++; } i=3; //check for the collision of a head with the body and tail while((snake_element_obj=snake_element_obj_arr.At(i))!=NULL) { //we don't check for the collision with the last snake's element, because it hasn't been moved yet if(snake_element_obj_arr.At(i+1)==NULL) break; if(snake_head_obj.GetPosX()==snake_element_obj.GetPosX() && snake_head_obj.GetPosY()==snake_element_obj.GetPosY()) { EventChartCustom(0,1,0,0,""); //hiding the snake's element we have collided snake_element_obj.Background(true); return CRASH_OBSTACLE_OR_SNAKE; } i++; } return CRASH_NO; } //+------------------------------------------------------------------+ //| Snake deletion | //+------------------------------------------------------------------+ void CSnakeGame::DeleteSnake() { delete snake_element_obj_arr; ChartRedraw(); }
뱀의 머리가 움직인 후 충돌 식별자를 반환하는 Check() 함수로 충돌 여부를 확인합니다.
뱀의 머리와 꼬리를 제대로 그려 이웃 요소의 위치에 따라 지정하기 위해 SetTrueSnake() 함수가 사용됩니다.
뱀의 음식
//+------------------------------------------------------------------+ //| Food creation | //+------------------------------------------------------------------+ void CSnakeGame::CreateFood() { int i; CChartFieldElement *food_obj; MathSrand(uint(TimeLocal())); //creating an object of CArrayObj class and assign it to the food_obj_arr properties of CSnakeGame class food_obj_arr=new CArrayObj(); i=0; while(i<COUNT_FOOD) { //creating the food food_obj=new CChartFieldElement; food_obj.Create(0,StringFormat(FOOD_BMP_LABEL_NAME,i),0,0,0); food_obj.BmpFileOn(IMG_FILE_NAME_FOOD); food_obj_arr.Add(food_obj); //setting the field coordinates on the field and moving it on the playing field FoodMoveOnField(i); i++; } } //+------------------------------------------------------------------+ //| Food movement method | //+------------------------------------------------------------------+ void CSnakeGame::FoodMoveOnChart() { CChartFieldElement *food_obj; int i; i=0; while((food_obj=food_obj_arr.At(i))!=NULL) { food_obj.Move(header_left,header_top+HEADER_HEIGHT); i++; } ChartRedraw(); } //+---------------------------------------------------------------------------+ //| A method to set coordinates of a food and to move it on the playing field | //+---------------------------------------------------------------------------+ void CSnakeGame::FoodMoveOnField(int food_num) { int i,j,k,n,m; CChartFieldElement *snake_element_obj,*food_obj; CChartObjectEdit *edit_obj; //setting a new value for "Foods left" on the status panel edit_obj=status_panel_obj_arr.At(1); edit_obj.Description(StringFormat(spaces2+FOOD_LEFT_OVER_EDIT_TEXT,MAX_LENGTH_SNAKE-snake_element_obj_arr.Total())); bool b; b=false; k=0; //generating randomly the food coordinates until the we get the free cells while(true) { //generating a row number i=(int)(MathRand()/32767.0*(COUNT_ROWS-1)); //generating a column number j=(int)(MathRand()/32767.0*(COUNT_COLUMNS-1)); n=0; //check, if there are any elements of the snake while((snake_element_obj=snake_element_obj_arr.At(n))!=NULL) { if(j!=snake_element_obj.GetPosX() && i!=snake_element_obj.GetPosY()) b=true; else { b=false; break; } n++; } //checking for the other food presence if(b==true) { n=0; while((food_obj=food_obj_arr.At(n))!=NULL) { if(j!=food_obj.GetPosX() && i!=food_obj.GetPosY()) b=true; else { b=false; break; } n++; } } //checking for the presence of the obstacle if(b==true && game_level[current_level][i][j]!=9) break; k++; } food_obj=food_obj_arr.At(food_num); //show food food_obj.Background(false); //setting new coordinates food_obj.SetPos(j,i); //moving the food food_obj.Move(header_left,header_top+HEADER_HEIGHT); ChartRedraw(); } //+------------------------------------------------------------------+ //| Food deletion | //+------------------------------------------------------------------+ void CSnakeGame::DeleteFood() { delete food_obj_arr; ChartRedraw(); }
필드에서의 음식 위치는 음식이 위치하는 셀이 다른 요소들을 포함하지 않는 한 무작위로 설정됩니다.
상태 패널
//+------------------------------------------------------------------+ //| Status Panel Creation | //+------------------------------------------------------------------+ void CSnakeGame::CreateStatusPanel() { CChartObjectEdit *edit_obj; //creating an object of CArrayObj class and assign it to the status_panel_obj_arr properties of CSnakeGame class status_panel_obj_arr=new CArrayObj(); //creating the "Level" edit edit_obj=new CChartObjectEdit; edit_obj.Create(0,LEVEL_EDIT_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); edit_obj.BackColor(STATUS_BACKGROUND); edit_obj.Color(STATUS_COLOR); edit_obj.Description(StringFormat(spaces6+LEVEL_EDIT_TEXT,current_level,MAX_LEVEL)); edit_obj.Selectable(false); edit_obj.ReadOnly(true); status_panel_obj_arr.Add(edit_obj); //creating the "Food left over" edit edit_obj=new CChartObjectEdit; edit_obj.Create(0,FOOD_LEFT_OVER_EDIT_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); edit_obj.BackColor(STATUS_BACKGROUND); edit_obj.Color(STATUS_COLOR); edit_obj.Description(StringFormat(spaces2+FOOD_LEFT_OVER_EDIT_TEXT,MAX_LENGTH_SNAKE-3)); edit_obj.Selectable(false); edit_obj.ReadOnly(true); status_panel_obj_arr.Add(edit_obj); //creating the "Lives" edit edit_obj=new CChartObjectEdit; edit_obj.Create(0,LIVES_EDIT_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); edit_obj.BackColor(STATUS_BACKGROUND); edit_obj.Color(STATUS_COLOR); edit_obj.Description(StringFormat(spaces8+LIVES_EDIT_TEXT,current_lives)); edit_obj.Selectable(false); edit_obj.ReadOnly(true); status_panel_obj_arr.Add(edit_obj); //moving the status panel StatusPanelMoveOnChart(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Status Panel movement method | //+------------------------------------------------------------------+ void CSnakeGame::StatusPanelMoveOnChart() { CChartObjectEdit *edit_obj; int x,y,i; x=header_left; y=header_top+HEADER_HEIGHT+COUNT_ROWS*(SQUARE_HEIGHT-1)+1; i=0; while((edit_obj=status_panel_obj_arr.At(i))!=NULL) { edit_obj.X_Distance(x+i*CONTROL_WIDTH); edit_obj.Y_Distance(y); i++; } ChartRedraw(); } //+------------------------------------------------------------------+ //| Status Panel deletion method | //+------------------------------------------------------------------+ void CSnakeGame::DeleteStatusPanel() { delete status_panel_obj_arr; ChartRedraw(); }
관리 패널
//+------------------------------------------------------------------+ //| Control Panel creation method | //+------------------------------------------------------------------+ void CSnakeGame::CreateControlPanel() { CChartObjectButton *button_obj; //creating an object of CArrayObj class and assign it to the control_panel_obj_arr properties of CSnakeGame class control_panel_obj_arr=new CArrayObj(); //creating the "Start" button button_obj=new CChartObjectButton; button_obj.Create(0,START_GAME_BUTTON_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); button_obj.BackColor(CONTROL_BACKGROUND); button_obj.Color(CONTROL_COLOR); button_obj.Description(START_GAME_BUTTON_TEXT); button_obj.Selectable(false); control_panel_obj_arr.Add(button_obj); //creating the "Pause" button button_obj=new CChartObjectButton; button_obj.Create(0,PAUSE_GAME_BUTTON_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); button_obj.BackColor(CONTROL_BACKGROUND); button_obj.Color(CONTROL_COLOR); button_obj.Description(PAUSE_GAME_BUTTON_TEXT); button_obj.Selectable(false); control_panel_obj_arr.Add(button_obj); //creating the "Stop" button button_obj=new CChartObjectButton; button_obj.Create(0,STOP_GAME_BUTTON_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); button_obj.BackColor(CONTROL_BACKGROUND); button_obj.Color(CONTROL_COLOR); button_obj.Description(STOP_GAME_BUTTON_TEXT); button_obj.Selectable(false); control_panel_obj_arr.Add(button_obj); //moving the control panel ControlPanelMoveOnChart(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Control Panel movement method | //+------------------------------------------------------------------+ void CSnakeGame::ControlPanelMoveOnChart() { CChartObjectButton *button_obj; int x,y,i; x=header_left; y=header_top+HEADER_HEIGHT+COUNT_ROWS*(SQUARE_HEIGHT-1)+1; i=0; while((button_obj=control_panel_obj_arr.At(i))!=NULL) { button_obj.X_Distance(x+i*CONTROL_WIDTH); button_obj.Y_Distance(y+CONTROL_HEIGHT); i++; } ChartRedraw(); } //+------------------------------------------------------------------+ //| Control Panel deletion method | //+------------------------------------------------------------------+ void CSnakeGame::DeleteControlPanel() { delete control_panel_obj_arr; ChartRedraw(); }
게임 초기화, 초기화 해제, 게임 요소 이동
//+------------------------------------------------------------------+ //| Game elements movement method | //+------------------------------------------------------------------+ void CSnakeGame::AllMoveOnChart() { FieldMoveOnChart(); StatusPanelMoveOnChart(); ControlPanelMoveOnChart(); ObstacleMoveOnChart(); SnakeMoveOnChart(); FoodMoveOnChart(); } //+------------------------------------------------------------------+ //| Game initialization | //+------------------------------------------------------------------+ void CSnakeGame::Init() { CreateHeader(); CreateField(); CreateStatusPanel(); CreateControlPanel(); CreateObstacle(); CreateSnake(); CreateFood(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Game deinitialization | //+------------------------------------------------------------------+ void CSnakeGame::Deinit() { DeleteFood(); DeleteSnake(); DeleteObstacle(); DeleteControlPanel(); DeleteStatusPanel(); DeleteField(); DeleteHeader(); }
게임 관리
//+------------------------------------------------------------------+ //| Dummy Start game method | //+------------------------------------------------------------------+ void CSnakeGame::StartGame() { return; } //+------------------------------------------------------------------+ //| Dummy Pause game method | //+------------------------------------------------------------------+ void CSnakeGame::PauseGame() { return; } //+------------------------------------------------------------------+ //| Stop game method | //+------------------------------------------------------------------+ void CSnakeGame::StopGame() { CChartObjectEdit *edit_obj; current_level=0; current_lives=LIVES_SNAKE; //setting new value for the "Level" field of the status panel edit_obj=status_panel_obj_arr.At(0); edit_obj.Description(StringFormat(spaces6+LEVEL_EDIT_TEXT,current_level,MAX_LEVEL)); //setting new value for the "Lives" field of the status panel edit_obj=status_panel_obj_arr.At(2); edit_obj.Description(StringFormat(spaces8+LIVES_EDIT_TEXT,current_lives)); DeleteFood(); DeleteSnake(); DeleteObstacle(); CreateObstacle(); CreateSnake(); CreateFood(); } //+------------------------------------------------------------------+ //| Level restart method | //+------------------------------------------------------------------+ void CSnakeGame::ResetGame() { CChartObjectEdit *edit_obj; if(current_lives-1==-1)StopGame(); else { //decreasing the number of lives current_lives--; //updating it at the status panel edit_obj=status_panel_obj_arr.At(2); edit_obj.Description(StringFormat(spaces8+LIVES_EDIT_TEXT,current_lives)); DeleteFood(); DeleteSnake(); CreateSnake(); CreateFood(); } } //+------------------------------------------------------------------+ //| Next level method | //+------------------------------------------------------------------+ void CSnakeGame::NextLevel() { CChartObjectEdit *edit_obj; current_lives=LIVES_SNAKE; //to the initial level if there isn't next level if(current_level+1>MAX_LEVEL)StopGame(); else { //else increasing the level and updating the startus panel contents current_level++; edit_obj=status_panel_obj_arr.At(0); edit_obj.Description(StringFormat(spaces6+LEVEL_EDIT_TEXT,current_level,MAX_LEVEL)); edit_obj=status_panel_obj_arr.At(2); edit_obj.Description(StringFormat(spaces8+LIVES_EDIT_TEXT,current_lives)); DeleteFood(); DeleteSnake(); DeleteObstacle(); CreateObstacle(); CreateSnake(); CreateFood(); } }
이벤트 처리 (최종 코드)
// Declaring and creating an object of CSnakeGame type at global level CSnakeGame snake_field; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { snake_field.Init(); EventSetTimer(1); return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { snake_field.Deinit(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnTimer() { //setting the buttons unpressed if(ObjectFind(0,START_GAME_BUTTON_NAME)>=0 && ObjectGetInteger(0,START_GAME_BUTTON_NAME,OBJPROP_STATE)==true) ObjectSetInteger(0,START_GAME_BUTTON_NAME,OBJPROP_STATE,false); if(ObjectFind(0,PAUSE_GAME_BUTTON_NAME)>=0 && ObjectGetInteger(0,PAUSE_GAME_BUTTON_NAME,OBJPROP_STATE)==true) ObjectSetInteger(0,PAUSE_GAME_BUTTON_NAME,OBJPROP_STATE,false); if(ObjectFind(0,STOP_GAME_BUTTON_NAME)>=0 && ObjectGetInteger(0,STOP_GAME_BUTTON_NAME,OBJPROP_STATE)==true) ObjectSetInteger(0,STOP_GAME_BUTTON_NAME,OBJPROP_STATE,false); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { long x,y; static bool press_key=true; static bool press_button=false; static bool move=false; //if key has been pressed and the snake has moved, let's specify the new movement direction if(id==CHARTEVENT_KEYDOWN && press_key==false) { if((lparam==VK_LEFT) && (snake_field.GetDirection()!=DIRECTION_LEFT && snake_field.GetDirection()!=DIRECTION_RIGHT)) snake_field.SetDirection(DIRECTION_LEFT); else if((lparam==VK_RIGHT) && (snake_field.GetDirection()!=DIRECTION_LEFT && snake_field.GetDirection()!=DIRECTION_RIGHT)) snake_field.SetDirection(DIRECTION_RIGHT); else if((lparam==VK_DOWN) && (snake_field.GetDirection()!=DIRECTION_UP && snake_field.GetDirection()!=DIRECTION_DOWN)) snake_field.SetDirection(DIRECTION_DOWN); else if((lparam==VK_UP) && (snake_field.GetDirection()!=DIRECTION_UP && snake_field.GetDirection()!=DIRECTION_DOWN)) snake_field.SetDirection(DIRECTION_UP); press_key=true; } //if "Start" button has been pressed and press_button=false if(id==CHARTEVENT_OBJECT_CLICK && sparam==START_GAME_BUTTON_NAME && press_button==false) { //waiting 1 second Sleep(1000); //generating new event for snake movement EventChartCustom(0,0,0,0,""); press_button=true; } //if "Pause" button has been pressed else if(id==CHARTEVENT_OBJECT_CLICK && sparam==PAUSE_GAME_BUTTON_NAME) { press_button=false; } //if "Stop" button has been pressed else if(id==CHARTEVENT_OBJECT_CLICK && sparam==STOP_GAME_BUTTON_NAME) { snake_field.StopGame(); press_key=true; press_button=false; } //processing of the snake movement event, if press_button=true else if(id==CHARTEVENT_CUSTOM && press_button==true) { snake_field.SnakeMoveOnField(); press_key=false; } //processing of the game restart event else if(id==CHARTEVENT_CUSTOM+1) { snake_field.ResetGame(); Sleep(1000); press_key=true; press_button=false; } //processing of the next level event else if(id==CHARTEVENT_CUSTOM+2) { snake_field.NextLevel(); Sleep(1000); press_key=true; press_button=false; } //processing of the header movement event else if(id==CHARTEVENT_OBJECT_DRAG && sparam==HEADER_BUTTON_NAME) { x=ObjectGetInteger(0,sparam,OBJPROP_XDISTANCE); y=ObjectGetInteger(0,sparam,OBJPROP_YDISTANCE); snake_field.SetHeaderPos(x,y); snake_field.AllMoveOnChart(); } } //+------------------------------------------------------------------+
press_key 및 press_button는 OnChartEvent 이벤트 처리 함수에 정의된 정적 변수 두개입니다.
만약 press_button 변수가 false이면 게임 시작이 허락됩니다. "시작" 버튼을 클릭하면 press_button 변수가 true로 설정됩니다(게임을 시작하는 코드의 재실행 금지). 이 상태는 다음 이벤트 중 하나가 발생할 때까지 그대로 유지됩니다.
- 현재 레벨 재시작;
- 다음 레벨로의 이동;
- 게임 일시정지 ("일시 정지" 버튼이 눌림);
- 게임 중단 ("그만하기" 버튼이 눌림).
뱀 움직임 방향의 변화는 뱀이 필드에서 움직인 후 지금 방향과 수직인 경우에 이루어집니다. (press_key 변수의 값이 알려줌) 이러한 조건은 CHARTEVENT_KEYDOWN 이벤트 처리 함수(키 누름 이벤트)에서 고려됩니다.
헤더를 변화시키면 CHARTEVENT_OBJECT_DRAG 이벤트가 생성되어 CSnakeGame 클래스의 header_left 및 header_top 필드가 재정의 됩니다. 다른 게임 요소의 이동은 이러한 필드의 값에 따라 결정됩니다.
필드의 이동은 TradePad_Sample에서 제시된 방식으로 구현됩니다.
마치며
이 문서에서 우리는 MQL5로 게임을 짜는 법에 대해서 알아보았습니다.
표준 라이브러리 클래스 (컨트롤 클래스), CArrayObj 클래스를 배웠고, 이벤트 핸들링을 통해 함수 호출을 주기적으로 처리하는 방법에 대해서 배웠습니다.
아래의 레퍼런스에서 "스네이크" 게임의 소스 코드 아카이브를 다운받을 수 있습니다. 저 아카이브는 terminal_data_folder에 압축해제되어야 합니다.
MetaQuotes 소프트웨어 사를 통해 러시아어가 번역됨.
원본 기고글: https://www.mql5.com/ru/articles/65