воскресенье, 1 апреля 2012 г.

Игра для тренировки памяти (часть 1)

Напишем хорошо известную игру для тренировки памяти. На поле расположены перевернутые картинки, переворачивая по две необходимо найти среди них одинаковые. Найденная пара исчезает с поля.
По задумке в программе должно быть:
1. Игровое поле 6х6 с картинками
2. Учет количество ходов (или времени)
3. Просмотр таблицы рекордов
4. Настройки: выбор цвета фона и набора картинок
5. Начальный экран


Игровое поле

1. Создаем новый проект (у меня он называется Memoria)

2. Для игрового поля будем использовать GridView. GridView это представление данных (то есть их отображение на экране). А сами данные хранятся и передаются в View в адаптере (BaseAdapter), который связан с этим GridView. Про адаптеры я рассказывала в Списках. Там адаптеры были для ListView, но для GridView все то же самое, так что повторять не буду. Стандартный адаптер нам не подойдет, поэтому будем писать свой. Им будем класс GridAdapter унаследованный от стандартного класса адаптеров BaseAdapter.

3. Сначала в main.xml добавляем GridView
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="fill_parent"
  4. android:layout_height="fill_parent"
  5. android:orientation="vertical" >
  6.  
  7. <GridView xmlns:android="http://schemas.android.com/apk/res/android" 
  8. android:id="@+id/field"
  9. android:layout_width="fill_parent" 
  10. android:layout_height="wrap_content"
  11. android:gravity="center"
  12. />
  13. </LinearLayout>

4. Создаем в проекте файл GridAdapter.java (в директории src/your.packagename/). Создаем в нем класс GridAdapter. Унаследованные от BaseAdapter классы должны определять четыре метода:
int getCount() - Возвращает количество элементов в GridView
Object getItem(int position) - Возвращает объект, хранящийся под номером position
long getItemId(int position) - Возвращает идентификатор элемента, хранящегося в под номером position
View getView(int position, View convertView, ViewGroup parent) - Возвращает View, который будет выведен в ячейке под номером position. Каждая ячейка идентифицируется не двумя цифрами (столбец и строка), как можно было ожидать, а одной. Нумеруются ячейки сверху вниз, слева направо. Пока описываем эти методы весьма условно, получаем что-то такое:
  1. class GridAdapter extends BaseAdapter
  2. {
  3.   private Context mContext;
  4.   private Integer mCols, mRows;
  5.  
  6.   public GridAdapter(Context context, int cols, int rows)
  7.   {
  8.     mContext = context;
  9.     mCols = cols;
  10.     mRows = rows;
  11.   }
  12.  
  13.   @Override
  14.   public int getCount() {
  15.     return mCols * mRows;
  16.   }
  17.  
  18.   @Override
  19.   public Object getItem(int position) {
  20.     return null;
  21.   }
  22.  
  23.   @Override
  24.   public long getItemId(int position) {
  25.     return 0;
  26.   }
  27.  
  28.   @Override
  29.   public View getView(int position, View convertView, ViewGroup parent) {
  30.  
  31.     ImageView view; // выводиться у нас будет картинка
  32.  
  33.     if (convertView == null)
  34.       view = new ImageView(mContext);
  35.     else
  36.       view = (ImageView)convertView;
  37.  
  38.     view.setImageResource(R.drawable.pict);
  39.  
  40.     return view;
  41.   }
  42. }

5. Теперь в основном классе связываем наш адаптер с таблицей:
  1. public class MemoriaActivity extends Activity {
  2.  
  3.   private GridView mGrid;
  4.   private GridAdapter mAdapter;
  5.  
  6.   @Override
  7.   public void onCreate(Bundle savedInstanceState) {
  8.     super.onCreate(savedInstanceState);
  9.     setContentView(R.layout.main);
  10.  
  11.     mGrid = (GridView)findViewById(R.id.field);
  12.     mGrid.setNumColumns(6);
  13.     mGrid.setEnabled(true);
  14.  
  15.     mAdapter = new GridAdapter(this66);
  16.     mGrid.setAdapter(mAdapter);
  17.   }
  18. }

6. Запускаем, получаем таблицу 6*6 с одинаковой картинкой в каждой ячейке.


7. Хорошо, но нам нужны разные картинки, причем каждая картинка должна встречаться два раза. Так как в будущем наборы картинок можно будет менять, для удобства в каждом таком наборе назовем файлы, например так: animal0.png, animal1.png, …. animal17.png или people0.png, people1.png, …. people17.png. Теперь наборы картинок отличаются префиксом, а внутри набора цифрой. Для обращения к картинке (или другого ресурса) используется его идентификатор, например R.drawable.picture. Для того чтобы выбирать картинку динамически воспользуемся классом Resources. Этот класс предоставляет доступ ко всем ресурсам приложения. Для инициализации экземпляра класса используется метод getResources () (из класса Context). А для получсения идентификатора ресурса по его названию метод
getIdentifier(String name, String defType, String defPackage)
где
name — название ресурса
defType — его тип (drawable, string, anim)
defPackage — имя пакета приложения (у меня это mj.android.memoria). Имя пакета можно также получить с помощью метода getPackageName() класса Context.

Таким образом записи
Integer identifierID = R.drawable.picture;
и
Resources mRes = mContext.getResources();
Integer identifierID = mRes.getIdentifier("picture", "drawable", mContext.getPackageName());

по сути одинаковые и возвращают идентификатор файла picture.png из директории drawable.

8. Картинки в таблице должны располагаться в случайном порядке, для этого удобно использовать метод Shuffle() класса ArrayList.
ArrayList — массив для хранения данных любого типа. Добавим в массив название каждой картинки по два раза, а затем вызывем метод Shuffle, который перемешает все элементы.

9. Допишим класс GridAdapter
  1. class GridAdapter extends BaseAdapter
  2. {
  3.   private Context mContext;
  4.   private Integer mCols, mRows;
  5.   private ArrayList<String> arrPict; // массив картинок
  6.   private String PictureCollection; // Префикс набора картинок
  7.   private Resources mRes; // Ресурсы приложени
  8.  
  9.   public GridAdapter(Context context, int cols, int rows)
  10.   {
  11.     mContext = context;
  12.     mCols = cols;
  13.     mRows = rows;
  14.     arrPict = new ArrayList<String>();
  15.     // Пока определяем префикс так, позже он будет браться из настроек
  16.     PictureCollection = "animal";
  17.     // Получаем все ресурсы приложения
  18.     mRes = mContext.getResources();
  19.  
  20.     // Метод заполняющий массив vecPict
  21.     makePictArray ();
  22.   }
  23.  
  24.   private void makePictArray () {
  25.     // очищаем вектор
  26.     arrPict.clear();
  27.     // добавляем
  28.     for (int i = 0; i < ((mCols * mRows) / 2); i++)
  29.     {
  30.       arrPict.add (PictureCollection + Integer.toString (i));
  31.       arrPict.add (PictureCollection + Integer.toString (i));
  32.     }
  33.     // перемешиваем
  34.     Collections.shuffle(arrPict);
  35.   }
  36.  
  37.   @Override
  38.   public int getCount() {
  39.     return mCols*mRows;
  40.   }
  41.  
  42.   @Override
  43.   public Object getItem(int position) {
  44.     return null;
  45.   }
  46.  
  47.   @Override
  48.   public long getItemId(int position) {
  49.     return 0;
  50.   }
  51.  
  52.   @Override
  53.   public View getView(int position, View convertView, ViewGroup parent) {
  54.  
  55.     ImageView view; // выводиться у нас будет картинка
  56.  
  57.     if (convertView == null)
  58.       view = new ImageView(mContext);
  59.     else
  60.       view = (ImageView)convertView;
  61.     // Получаем идентификатор ресурса для картинки,
  62.     // которая находится в векторе vecPict на позиции position
  63.     Integer drawableId = mRes.getIdentifier(arrPict.get(position)"drawable", mContext.getPackageName());
  64.     view.setImageResource(drawableId);
  65.     return view;
  66.   }
  67. }

10. Все новые места кода прокомментированы, так что по идее все должно быть понятно. Запускаем. Видим вот такой вот веселенький зоопарк :)


11. Каждая ячейка может быть в одном из трех состояний: открытая, закрытая и удаленая (если две открытые картинки совпали, они убираются с поля). Для удобства добавим в GridAdapter константы для этих состояний:

private static final int CELL_CLOSE = 0;
private static final int CELL_OPEN = 1;
private static final int CELL_DELETE = -1;

или enum (он красивее смотрится)

private static enum Status {CELL_OPEN, CELL_CLOSE, CELL_DELETE};

В адаптере создадим массив для хранения состояния каждой ячейки, вначале игры всем ячейкам проставляется статус закрыта (CELL_CLOSE). И немного исправим метод getView чтобы картинка рисовалась в зависимости от статуса
  1. class GridAdapter extends BaseAdapter
  2. {
  3.   private Context mContext;
  4. ...
  5.   private static enum Status {CELL_OPEN, CELL_CLOSE, CELL_DELETE};
  6.   private ArrayList<Status> arrStatus; // состояние ячеек
  7.  
  8.   public GridAdapter(Context context, int cols, int rows)
  9.   {
  10.     mContext = context;
  11.     mCols = cols;
  12.     mRows = rows;
  13.     arrPict = new ArrayList<String>();
  14.     arrStatus = new ArrayList<Status>();
  15.  
  16.     // Пока определяем префикс так, позже он будет браться из настроек
  17.     PictureCollection = "animal";
  18.     // Получаем все ресурсы приложения
  19.     mRes = mContext.getResources();
  20.  
  21.     // Метод заполняющий массив vecPict
  22.     makePictArray ();
  23.     // Метод устанавливающий всем ячейкам статус CELL_CLOSE
  24.     closeAllCells();
  25.   }
  26.  
  27. ...
  28.  
  29.   private void closeAllCells () {
  30.     arrStatus.clear();
  31.     for (int i = 0; i < mCols * mRows; i++)
  32.       arrStatus.add(Status.CELL_CLOSE);
  33.   }
  34.  
  35.   @Override
  36.   public View getView(int position, View convertView, ViewGroup parent) {
  37.  
  38.     ImageView view; // выводиться у нас будет картинка
  39.  
  40.     if (convertView == null)
  41.       view = new ImageView(mContext);
  42.     else
  43.       view = (ImageView)convertView;
  44.  
  45.     switch (arrStatus.get(position))
  46.     {
  47.       case CELL_OPEN:
  48.         // Получаем идентификатор ресурса для картинки,
  49.         // которая находится в векторе vecPict на позиции position
  50.         Integer drawableId = mRes.getIdentifier(arrPict.get(position)"drawable", mContext.getPackageName());
  51.         view.setImageResource(drawableId);
  52.         break;
  53.       case CELL_CLOSE:
  54.         view.setImageResource(R.drawable.close);
  55.         break;
  56.       default:
  57.         view.setImageResource(R.drawable.none);        
  58.     }
  59.  
  60.     return view;
  61.   }
  62. }

12. Теперь надо обработать нажатие на ячейку.
По нажатию будем делать следующее:
1. Проверяем есть ли уже открытые ячейки. Если открыты две ячейки, то сравниваем их и если они одинаковые удаляем, если разные — закрываем.
4. Открываем выбранную ячейку.
5. Проверяем остались ли открытые ячейки, если нет, то заканчиваем игру
Можно проверять открытые картинки после нажатия, но тогда они будут закрываться (или удаляться) слишком быстро, что не удобно.

Нажатие на ячейку в таблице обрабатывает метод setOnItemClickListener класса GridView
  1. public class MemoriaActivity extends Activity {
  2.  
  3.  
  4.   public void onCreate(Bundle savedInstanceState) {
  5.     super.onCreate(savedInstanceState);
  6.     setContentView(R.layout.main);
  7.  
  8.     ...
  9.  
  10.     mGrid.setOnItemClickListener(new OnItemClickListener() {
  11.       @Override
  12.       public void onItemClick(AdapterView<?> parent, View v,int position, long id){
  13.  
  14.         mAdapter.checkOpenCells ();
  15.         mAdapter.openCell (position);
  16.  
  17.         if (mAdapter.checkGameOver())
  18.             Toast.makeText (getApplicationContext()"Игра закончена", Toast.LENGTH_SHORT).show(); 
  19.       }
  20.     });
  21.   }
  22. }

В методе setOnItemClickListener мы дергаем три метода из GridAdapter, он достаточно банальны:
  1. class GridAdapter extends BaseAdapter
  2. {
  3.   ...
  4.  
  5.   public void checkOpenCells() {
  6.     int first = arrStatus.indexOf(Status.CELL_OPEN);
  7.     int second = arrStatus.lastIndexOf(Status.CELL_OPEN);
  8.     if (first == second)
  9.       return;
  10.     if (arrPict.get(first).equals (arrPict.get(second)))
  11.     {
  12.       arrStatus.set(first, Status.CELL_DELETE);
  13.       arrStatus.set(second, Status.CELL_DELETE);
  14.     }
  15.     else
  16.     {
  17.       arrStatus.set(first, Status.CELL_CLOSE);
  18.       arrStatus.set(second, Status.CELL_CLOSE);
  19.     }
  20.     return;
  21.   }
  22.  
  23.   public void openCell(int position) {
  24.     if (arrStatus.get(position) != Status.CELL_DELETE)
  25.       arrStatus.set(position, Status.CELL_OPEN);
  26.  
  27.     notifyDataSetChanged(); 
  28.     return;
  29.   }
  30.  
  31.   public boolean checkGameOver() {
  32.     if (arrStatus.indexOf(Status.CELL_CLOSE) < 0)
  33.       return true;
  34.     return false;
  35.   }
  36. }

Метод notifyDataSetChanged() класса BaseAdapter (мы его вызываем в public void openCell(int position)) сообщает GridView, что данные изменились и таблица перерисовывается. Казалось бы, что notifyDataSetChanged() надо вызвать и в checkOpenCells(), потому что там тоже меняется статус ячеек, но смысла в этом мало, потому что после checkOpenCells() всегда будет вызываться openCell() в которой таблица и перерисуется.

13. Все! Можно запускать и играть :) Пока игра не ведет счета и после окончания игры просто показывает черный экран без возможности запуска новой игры. Все это мы сделаем в следующий раз.


Исходники:
Memoria.zip

Все части урока:
1. Игровое поле 6х6 с картинками (часть 1)
2. Учет количество ходов (или времени) (часть 2)
3. Просмотр таблицы рекордов (часть 4)
4. Настройки: выбор цвета фона и набора картинок (часть 3)
5. Начальный экран (Часть 2)

Комментариев нет:

Отправить комментарий