Введение
Добрый день, господа! Я ученик 11 класса и разработкой занимаюсь только от того, что не хочу готовиться к ЕГЭ. Хочется отметить, что статья не предназначена для новичков в программировании. Идею приложения можно описать тремя словами — увидел, запомнил, повторил. Перед игроком появляется квадратное поле с определённым число закрашенных элементов. Через некоторое время поле очищается. Надо выбрать какие элементы были закрашены. По мере прохождения уровней игры поле становится всё больше и запоминать приходится всё больше.
Теория
Для начала нам нужно определиться с архитектурой программного интерфейса. Я предпочитаю MVC (Model — View — Controller).
При таком подходе контроллер перехватывает событие извне и в соответствии с заложенной в него логикой, реагирует на это событие изменяя модель, посредством вызова соответствующего метода. После изменения модель использует событие о том что она изменилась, и все подписанные на это события представления, получив его, обращаются к Модели за обновленными данными, после чего их и отображают. Так, определились. Дальше нужно выбрать язык программирования. Мне нравиться Java. Java — это объектно ориентированный язык программирования, поэтому нам следует придерживаться принципов ООП:
1) Инкапсуляция — это компонент позволяющий объединить код и данный которыми он манипулирует.
2) Полиморфизм — свойство системы, позволяющее использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта.
3) Наследование — свойство системы, позволяющее описать новый класс на основе уже существующего с частично или полностью заимствующейся функциональностью. Класс, от которого производится наследование, называется базовым или родительским классом. Новый класс — потомком, наследником, дочерним или производным классом.
Теперь ограничим предметную область такими классами как:
Игра (Game), Игрок (Player), Поле (Field). Игра отвечает за данные формируемые непосредственно в игре. Поле хранит информацию о том, какие ячейки закрашены, а какие нет. Игра умеет создавать поле. Игрок умеет обрабатывать пользовательские данные, т.е. сохранять очки и рейтинг, когда это необходимо.
Основная часть
Хватит теории. Нам нужно выполнить следующую последовательность действий:
1) Скачать среду разработки и JDK.
2) Установить оба комплекта себе на компьютер.
3) Запустить среду разработки.
Создадим пустой проект и начнём творить. Определим структура пакетов таким образом:
Дальше мы проделываем небольшой трюк. Создаём файлы разметки:
Xml файл root отвечает за корень нашего представления. Внутри него будут динамически меняться разметки фрагментов, которые мы уже создали. Соответственно в пакете представления (view) создадим классы фрагменты:
Каждый фрагмент отвечает за один режим игры. При открытии приложения выполняется метод onCreate класса MainActivity(который мы тоже положили в пакет view). Изменим это метод до такого состояния:
Тут мы проверяем, что если наша активность создаётся первый раз, то нужно создать объекты фрагментов и сделать видимым фрагмент меню. Конечно прежде чем создавать объекты нужно задать им ссылки, но я думаю, что с этим вы сами справитесь.
Фрагменты созданы, но они ничего не делают. Самое время придать вдунуть в них жизнь. Создадим интерфейс:
public interface SendingActivity {
//Сообщает активности, какой пункт меню выбран
void send(int a);
//Сообщает активности, какой какое поле и какой уровень нужно запустить
void send(Field field, int level);
//Сообщает активности, что нужно запустить следующий уровень, соответствующего режима игры
void nextLevel(GameModChallenge game);
//Сообщает активности, что нужно запустить следующий уровень, соответствующего режима игры
void nextLevel(GameModSprint game);
//Сообщает активности, что нужно запустить следующий уровень, соответствующего режима игры
void nextLevel(GameModTwisting game);
//Сообщает активности, что нужно запустить следующий уровень, соответствующего режима игры
void nextLevel(GameModEvidence game);
//В данном режиме игры не при первом корректном вводе будет запускаться следующий уровень, поэтому нужен другой метод
void next(GameModChallenge game);
//Сообщает активности, что пользователь ошибся
void failSprint(GameModSprint game);
//Сообщает активности, что пользователь ошибся
void failTwisting(GameModTwisting game);
//Сообщает активности, что пользователь ошибся
void failEvidence(GameModEvidence game);
//Сообщает активности, что нужно обновить пользовательские данные
void releaseToolbar();
}
Через методы данного интерфейса объекты будут сообщать о своих действиях активности, а она уже будет решать, что ей делать.
Создадим фрагмент главного меню:
public final class MainMenuFragment extends Fragment implements View.OnClickListener {
Button sprint, challenge, twisting,evidence, exit;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.menu, container, false);
//region findViewById
sprint = (Button) view.findViewById(R.id.sprint);
challenge = (Button) view.findViewById(R.id.challenge);
twisting = (Button) view.findViewById(R.id.twist);
evidence = (Button) view.findViewById(R.id.evidence);
exit = (Button) view.findViewById(R.id.exit);
//endregion
//region setOnClickListener
sprint.setOnClickListener(this);
challenge.setOnClickListener(this);
twisting.setOnClickListener(this);
evidence.setOnClickListener(this);
exit.setOnClickListener(this);
//endregion
return view;
}
@Override
public void onClick(View view) {
int id = view.getId();
switch (id) {
case R.id.sprint: {
SendingActivity mess = (SendingActivity) getActivity();
mess.send(0);
break;
}
case R.id.challenge: {
SendingActivity mess = (SendingActivity) getActivity();
mess.send(1);
break;
}
case R.id.twist: {
SendingActivity mess = (SendingActivity) getActivity();
mess.send(2);
break;
}
case R.id.evidence: {
SendingActivity mess = (SendingActivity) getActivity();
mess.send(3);
break;
}
case R.id.exit: {
SendingActivity mess = (SendingActivity) getActivity();
mess.send(4);
break;
}
}
}
}
Тут я создал нашёл кнопки и повесил на них слушателя событий. В слушателе выбирается какая кнопка была нажата. В зависимости от этого объекту интерфейса посылается нужное сообщение. Но наша активность ещё не реагирует на посылаемые фрагментом сообщения. Исправим это позже, а сейчас напишем логику нашей игры. Наша логика содержится в пакете model. Там создадим вспомогательный класс Field:
public final class Field {
private int rightAnswer;
private int sizeField;
private int paintedSquare;
private int square[][];
private Random randX;
private Random randY;
public int[][] getSquare() {
return square;
}
public Field createSquare(int sizeField, int paintedSquare) {
this.sizeField = sizeField;
this.paintedSquare = paintedSquare;
this.square = new int[sizeField][sizeField];
randX = new Random(System.nanoTime());
randY = new Random(System.nanoTime());
for (int i = 0; i < sizeField; i++) {
for (int j = 0; j < sizeField; j++) {
if (paintedSquare > 0) {
paintedSquare--;
this.square[i][j] = 1;
} else {
this.square[i][j] = 0;
}
}
}
for (int i = 0; i < sizeField; i++) {
for (int j = 0; j < sizeField; j++) {
mixing(square);
}
}
return this;
}
//region mixing
private void mixing(int[][] arr) { //Метод,который перемешивает массив
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length; j++) {
swap(arr, i, j, randX.nextInt(sizeField), randY.nextInt(sizeField));
}
}
}
private void swap(int[][] arr, int i, int j, int newI, int newJ) {
int tmp = arr[i][j];
arr[i][j] = arr[newI][newJ];
arr[newI][newJ] = tmp;
}
//endregion
public int getSizeField() {
return sizeField;
}
public int getRightAnswer() {
return rightAnswer;
}
public void setRightAnswer(int rightAnswer) {
this.rightAnswer = rightAnswer;
}
public int getPaintedSquare() {
return paintedSquare;
}
}
Как вы видите этот класс нам нужен для работы с массивом правильных ответов. Если элемент массива содержит 1, то клетка закрашена. Поле умеет себя создавать. После создания оно себя сразу перемешивает. Теперь создадим базовый класс Game для режимов игры.
public abstract class Game {
private Field field;
private int currentLevel;
private Activity a;
Game(Activity a) {
this.a = a;
}
Activity getActivity() {
return a;
}
public int getCurrentLevel() {
return currentLevel;
}
public void setCurrentLevel(int currentLevel) {
this.currentLevel = currentLevel;
}
public void setField(Field field) {
this.field = field;
}
public Field getField() {
return field;
}
public Field createFieldInLevel(int level) {
switch (level) {
case 1: {
field = new Field().createSquare(3, 2);
break;
}
case 2: {
field = new Field().createSquare(3, 3);
break;
}
case 3: {
field = new Field().createSquare(4, 4);
break;
}
case 4: {
field = new Field().createSquare(4, 6);
break;
}
case 5: {
field = new Field().createSquare(4, 8);
break;
}
case 6: {
field = new Field().createSquare(5, 10);
break;
}
case 7: {
field = new Field().createSquare(5, 12);
break;
}
case 8: {
field = new Field().createSquare(6, 14);
break;
}
case 9: {
field = new Field().createSquare(6, 16);
break;
}
case 10: {
field = new Field().createSquare(7, 18);
break;
}
case 11: {
field = new Field().createSquare(7, 20);
break;
}
case 12: {
field = new Field().createSquare(7, 22);
break;
}
case 13: {
field = new Field().createSquare(7, 24);
break;
}
case 14: {
field = new Field().createSquare(8, 26);
break;
}
case 15: {
field = new Field().createSquare(8, 28);
break;
}
case 16: {
field = new Field().createSquare(8, 30);
break;
}
case 17: {
field = new Field().createSquare(8, 32);
break;
}
case 18: {
field = new Field().createSquare(9, 34);
break;
}
case 19: {
field = new Field().createSquare(9, 36);
break;
}
case 20: {
field = new Field().createSquare(9, 40);
break;
}
case 21: {
field = new Field().createSquare(10, 42);
break;
}
case 22: {
field = new Field().createSquare(10, 44);
break;
}
case 23: {
field = new Field().createSquare(10, 46);
break;
}
case 24: {
field = new Field().createSquare(10, 48);
break;
}
case 25: {
field = new Field().createSquare(10, 50);
break;
}
}
return field;
}
public abstract void createVictoryDialog(LayoutInflater inflater);
public abstract void createLooseDialog(LayoutInflater inflater);
public abstract void showDialog();
}
Данный класс определяет базовую функциональность режимов игры. Он умеет создавать поле, устанавливать текущий уровень игры, создавать диалоговые окна (именно для этого ему и нужна ссылка на активность) и показывать их. Объекты данного класса мы создавать не можем, поэтому создадим классы наследники. К примеру класс GameModSprint:
private AlertDialog.Builder builder;
private AlertDialog alert;
public GameModSprint(Activity a) {
super(a);
}
@Override
public void createVictoryDialog(LayoutInflater inflater) {
builder = new AlertDialog.Builder(getActivity());
builder.setCancelable(false);
View dialogV = inflater.inflate(R.layout.dialog_view, null, false);
View dialogTitle = inflater.inflate(R.layout.dialog_title, null, false);
TextView title = (TextView) dialogTitle.findViewById(R.id.dialog_title);
TextView message = (TextView) dialogTitle.findViewById(R.id.message);
message.setText(getActivity().getResources().getString(R.string.progress)+ getCurrentLevel());
title.setText(getActivity().getResources().getString(R.string.good));
title.setText(getActivity().getResources().getString(R.string.good));
ImageView imageView = (ImageView) dialogV.findViewById(R.id.image);
imageView.setImageResource(R.drawable.victory);
ImageButton next = (ImageButton) dialogV.findViewById(R.id.next);
ImageButton restart = (ImageButton) dialogV.findViewById(R.id.restart);
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.next: {
SendingActivity send = (SendingActivity) getActivity();
send.nextLevel(self());
alert.dismiss();
break;
}
case R.id.restart: {
SendingActivity send = (SendingActivity) getActivity();
send.failSprint(self());
alert.dismiss();
break;
}
case R.id.in_menu: {
alert.dismiss();
getActivity().onBackPressed();
break;
}
}
}
};
next.setOnClickListener(listener);
restart.setOnClickListener(listener);
ImageButton inMenu = (ImageButton) dialogV.findViewById(R.id.in_menu);
inMenu.setOnClickListener(listener);
builder.setView(dialogV);
builder.setCustomTitle(dialogTitle);
}
@Override
public void createLooseDialog(LayoutInflater inflater) {
builder = new AlertDialog.Builder(getActivity());
builder.setCancelable(false);
View dialogV = inflater.inflate(R.layout.dialog_view, null, false);
View dialogTitle = inflater.inflate(R.layout.dialog_title, null, false);
TextView title = (TextView) dialogTitle.findViewById(R.id.dialog_title);
TextView message = (TextView) dialogTitle.findViewById(R.id.message);
message.setText(getActivity().getResources().getString(R.string.not_progress)+ getCurrentLevel());
title.setText(getActivity().getResources().getString(R.string.bad));
ImageView imageView = (ImageView) dialogV.findViewById(R.id.image);
imageView.setImageResource(R.drawable.loose);
ImageButton next = (ImageButton) dialogV.findViewById(R.id.next);
ImageButton restart = (ImageButton) dialogV.findViewById(R.id.restart);
next.setBackgroundColor(getActivity().getResources().getColor(R.color.background_no_access));
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.restart: {
SendingActivity send = (SendingActivity) getActivity();
send.failSprint(self());
alert.dismiss();
break;
}
case R.id.in_menu: {
alert.dismiss();
getActivity().onBackPressed();
break;
}
}
}
};
next.setOnClickListener(listener);
restart.setOnClickListener(listener);
ImageButton inMenu = (ImageButton) dialogV.findViewById(R.id.in_menu);
inMenu.setOnClickListener(listener);
builder.setView(dialogV);
builder.setCustomTitle(dialogTitle);
}
@Override
public void showDialog() {
alert = builder.create();
alert.show();
}
private GameModSprint self() {
return this;
}
}
Этот класс определяет каким образом будут появляться диалоговые окна. Дело в том, что не всегда мне нужно просто перенести игрока на следующий уровень. К примеру в других режимах игры я захочу сделать прибавление рейтинга игроку или ещё какие-либо манипуляции с пользовательскими данными которые нужно выполнять только в данном режиме игры. Остальные классы создаются аналогичным образом.
И наконец мы можем реализовать наш интерфейс.
@Override
public void send(int a) {
switch (a) {
case 0: {
sprintFragment = new SprintFragment();
game = new GameModSprint(this);
game.createFieldInLevel(1);
game.setCurrentLevel(1);
sprintFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, sprintFragment).addToBackStack(null).commit();
break;
}
case 1: {
getFragmentManager().beginTransaction().replace(R.id.activity_main, levelChallengeFragment).addToBackStack(null).commit();
break;
}
case 2: {
twistingFragment = new TwistingFragment();
game = new GameModTwisting(this);
game.createFieldInLevel(1);
game.setCurrentLevel(1);
twistingFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, twistingFragment).addToBackStack(null).commit();
break;
}
case 3: {
evidenceFragment = new EvidenceFragment();
game = new GameModEvidence(this);
game.createFieldInLevel(1);
game.setCurrentLevel(1);
evidenceFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, evidenceFragment).addToBackStack(null).commit();
break;
}
case 4: {
finish();
break;
}
}
}
@Override
public void send(Field field, int level) {
challengeFragment = new ChallengeFragment();
game = new GameModChallenge(this);
game.setField(field);
game.setCurrentLevel(level);
challengeFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, challengeFragment).commit();
}
@Override
public void nextLevel(GameModChallenge game) {
challengeFragment = new ChallengeFragment();
game.setCurrentLevel(game.getCurrentLevel() + 1);
game.createFieldInLevel(game.getCurrentLevel());
textPoint.setText(String.valueOf(point));
textRating.setText(String.valueOf(rating));
challengeFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, challengeFragment).commit();
}
@Override
public void nextLevel(GameModSprint game) {
sprintFragment = new SprintFragment();
textPoint.setText(String.valueOf(point));
game.setCurrentLevel(game.getCurrentLevel() + 1);
game.createFieldInLevel(game.getCurrentLevel());
sprintFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, sprintFragment).commit();
}
@Override
public void nextLevel(GameModTwisting game) {
twistingFragment = new TwistingFragment();
textPoint.setText(String.valueOf(point));
game.setCurrentLevel(game.getCurrentLevel() + 1);
game.createFieldInLevel(game.getCurrentLevel());
twistingFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, twistingFragment).commit();
}
@Override
public void nextLevel(GameModEvidence game) {
evidenceFragment = new EvidenceFragment();
textPoint.setText(String.valueOf(point));
game.setCurrentLevel(game.getCurrentLevel() + 1);
game.createFieldInLevel(game.getCurrentLevel());
evidenceFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, evidenceFragment).commit();
}
@Override
public void next(GameModChallenge game) {
//Возможно,что игрок потратил игровую валюту
textPoint.setText(String.valueOf(point));
challengeFragment = new ChallengeFragment();
game.createFieldInLevel(game.getCurrentLevel());
challengeFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, challengeFragment).commit();
}
@Override
public void failSprint(GameModSprint game) {
sprintFragment = new SprintFragment();
game = new GameModSprint(this);
game.createFieldInLevel(1);
game.setCurrentLevel(1);
sprintFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, sprintFragment).commit();
}
@Override
public void failTwisting(GameModTwisting game) {
twistingFragment = new TwistingFragment();
game = new GameModTwisting(this);
game.createFieldInLevel(1);
game.setCurrentLevel(1);
twistingFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, twistingFragment).commit();
}
@Override
public void failEvidence(GameModEvidence game) {
evidenceFragment = new EvidenceFragment();
game = new GameModEvidence(this);
game.createFieldInLevel(1);
game.setCurrentLevel(1);
evidenceFragment.setGame(game);
getFragmentManager().beginTransaction().replace(R.id.activity_main, evidenceFragment).commit();
}
Теперь у фрагмента игрового уровня есть всё необходимое для создания. Он знает о текущем поле всё, что ему нужно. Осталось только связать наши данные с пользовательским интерфейсом.
public final class SprintFragment extends Fragment implements View.OnClickListener {
private Game game;
private Field field;
private int fieldSize;
private Button sq[][];
private LayoutInflater inflater;
private Runnable runner;
private View root;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
root = inflater.inflate(R.layout.game, container, false);
TextView title = (TextView) root.findViewById(R.id.game_challenge_title);
title.setText(String.valueOf(getActivity().getResources().getString(R.string.current_level) + game.getCurrentLevel()));
this.inflater = inflater;
final View.OnClickListener listener = this;
int doNotDuplicateId = 0;
final TableLayout fieldView = (TableLayout) root.findViewById(R.id.field);
ProgressBar progressBar = (ProgressBar) root.findViewById(R.id.progress_bar);
progressBar.setVisibility(View.INVISIBLE);
fieldSize = field.getSizeField();
sq = new Button[fieldSize][fieldSize];
fieldView.setWeightSum(fieldSize);
TableRow tableRows[] = new TableRow[fieldSize];
TableRow.LayoutParams paramsRow = new TableRow.LayoutParams();
paramsRow.height = TableRow.LayoutParams.MATCH_PARENT;
paramsRow.width = TableRow.LayoutParams.WRAP_CONTENT;
paramsRow.weight = 1;
for (int i = 0; i < fieldSize; i++) {
tableRows[i] = new TableRow(getActivity().getApplicationContext());
tableRows[i].setLayoutParams(paramsRow);
for (int j = 0; j < fieldSize; j++) {
sq[i][j] = (Button) inflater.inflate(R.layout.one_field_button, null, false);
sq[i][j].setId(doNotDuplicateId);
sq[i][j].setWidth(getActivity().getWindow().getWindowManager().getDefaultDisplay().getWidth() / fieldSize - getActivity().getWindow().getWindowManager().getDefaultDisplay().getWidth() / (fieldSize * fieldSize));
sq[i][j].setHeight(getActivity().getWindow().getWindowManager().getDefaultDisplay().getWidth() / fieldSize - getActivity().getWindow().getWindowManager().getDefaultDisplay().getWidth() / (fieldSize * fieldSize));
doNotDuplicateId++;
if (field.getSquare()[i][j] == 1) {
sq[i][j].setBackgroundDrawable(getActivity().getApplicationContext().getResources().getDrawable(R.drawable.sq_painted));
}
tableRows[i].addView(sq[i][j], j);
tableRows[i].setWeightSum(fieldSize);
}
fieldView.addView(tableRows[i], i);
}
root.postDelayed(runner = new Runnable() {
@Override
public void run() {
for (int i = 0; i < fieldSize; i++) {
for (int j = 0; j < fieldSize; j++) {
sq[i][j].setOnClickListener(listener);
if (field.getSquare()[i][j] == 1) {
sq[i][j].setBackgroundDrawable(getActivity().getApplicationContext().getResources().getDrawable(R.drawable.sq));
}
}
}
}
}, 2000L);
return root;
}
public void setGame(Game game) {
this.game = game;
this.field = game.getField();
}
@Override
public void onClick(View view) {
int id = view.getId();
for (int i = 0; i < fieldSize; i++) {
for (int j = 0; j < fieldSize; j++) {
if (id == sq[i][j].getId()) {
System.out.println(sq[i][j].getId());
if (field.getSquare()[i][j] == 1) {
field.setRightAnswer(field.getRightAnswer() + 1);
view.setOnClickListener(null);
view.setBackgroundDrawable(getActivity().getApplicationContext().getResources().getDrawable(R.drawable.sq_painted));
if (field.getRightAnswer() == field.getPaintedSquare()) {
game.createVictoryDialog(inflater);
game.showDialog();
}
} else if (field.getSquare()[i][j] == 0) {
view.setBackgroundDrawable(getActivity().getApplicationContext().getResources().getDrawable(R.drawable.sq_wrong));
game.createLooseDialog(inflater);
game.showDialog();
}
}
}
}
}
@Override
public void onStop() {
super.onStop();
root.removeCallbacks(runner);
}
}
Обратите внимание, на этот метод:
public void setGame(Game game) {
this.game = game;
this.field = game.getField();
}
С помощью него активность передаёт ссылку на игра фрагменту. Также, чтобы не использовать длинных точечных нотация создадим отдельную ссылку для поля.
Код в целом очень простой. Создаются массив кнопок, заполняется. Получается, что индексы двумерного массива кнопок совпадают с индексами двумерного массива из объекта класса Field.
Таким образом при определении нажатой кнопки нужно проверить какая цифра лежит в массиве объекта класса Fild. Если 0, то пользователь сделал неверный ввод и надо закончить игру вызовом
game.createLooseDialog(inflater);
game.showDialog();
который создаст и откроет нужное нам диалоговое окно с выборов действий.
Если игрок выбрал все элементы равное 1, то создаётся победное диалоговое окно. Несмотря на то, что у нас ссылка типа Game объект создавался активностью типа GameModSprint, значит и вызовется соответствующая переопределённая версия этого метода:
if (field.getRightAnswer() == field.getPaintedSquare()) {
game.createVictoryDialog(inflater);
game.showDialog();
}
Таким образом мы пользуемся мощнейшим средством языка Java — динамической диспетчеризацией методов.
Заключение
Итак, мы с вами познакомились с разработкой мобильных приложений. Надеюсь каждый прочитавший мою статью сможет написать своё приложение, хоть я и не останавливался на каждой мелочи. Удачи всем!
Автор: ureneva1999