ТЗ на примере «Охотника»***
Концепция игры: мини-игра, в которой охотник, сидя на лодке, подстреливает уток. Игрок управляет ружьём, ружьё наклоняется и производит выстрелы в зависимости от касаний игрока:
- заряд уже вылетел из ружья и находится на экране, ружьё наклоняется в сторону касания, но выстрел не производится
- заряд покинул экран, либо «убил» утку, т.е. заряд невидим => при касании помимо наклона к точке касания происходит выстрел, заряд покидает ствол ружья
Можно оставить только второй вариант, это проще.
1. Игровые объекты, моделируемые объектами соответствующих классов
1) Утка. Поля: x, y, width, height, velocityX, velocityY, visible (утка либо видна на экране, либо ее сбили), paint, bitmap. В поле bitmap хранится «скин» утки. Velocity-поля хранят значение горизонтальной и вертикальной скорости. Поле paint здесь и далее нужно просто чтобы передать в метод canvas.drawBitmap, больше никакой надобности в нем нет. Методы: draw(canvas: Canvas), в этот метод передаётся canvas view-шечки, на которой отрисовывается вся игра; move(time: Double) – метод меняет координаты в зависимости от временного промежутка; changeBitmap(bitmap: Bitmap) нужен, чтобы поменять «скин», когда утка коснётся края экрана и полетит в обратную сторону, чтобы объект не летел задом наперёд.
2) Заряд. Те же поля, что и в случае 1, за исключением bitmap. Отдельного «скина» нет, вместо этого появляется целочисленное поле radius. Методов два: draw(canvas: Canvas) и move(time: Double), в draw происходит отрисовка круга заданного целочисленного радиуса radius.
3) Ружьё. Те же поля, что и в случае 1, за исключением velocityX и velocityY, а также за исключением visible (оно здесь не нужно, ружьё всегда видно игроку). Появляется новое поле bulletSpeed. Методы: draw(canvas: Canvas), changeBitmap (метод нужен чтобы вернуть первоначальное положение ружью после выстрела) и fire(angle: Double, bullet: Bullet) – метод принимает угол наклона и, исходя из него, высчитывает горизонтальную и вертикальные компоненты скорости заряда bullet по обычным формулам: vX = bulletSpeed * Math.sin(angle); vY = - bulletSpeed * Math.cos(angle). Поворот ружья осуществляется так: создается матрица (android.graphics.matrix), в нее передается значение angle * 180 / Math.PI (значение угла в радианах вместо градусов), на основе этой матрицы создаётся повёрнутый битмап Bitmap.createBitmap(bmp, someX, someY, gunWidth, gunHeight, matrix, true).
При выстреле значение переменной visible объекта-заряда устанавливается в true.
Пример метода-выстрела:
fun fire(angle: Double, bullet: Bullet, bmp: Bitmap = bitmap) {
val vX = bulletSpeed * Math.sin(angle)
val vY = - bulletSpeed * Math.cos(angle)
val matrix = Matrix()
val angleDegrees = angle * 180 / Math.PI
matrix.postRotate(angleDegrees.toFloat())
bitmap = Bitmap.createBitmap(bmp, 0, 0, width, height, matrix, true)
bullet.visible = true
bullet.vX = vX.toInt()
bullet.vY = vY.toInt()
}
2. Отрисовка и игровой процесс:
- либо в кастомном вью внутри фрагмента внутри активити, либо в кастомном вью внутри активити (особенно, если ориентация жестко задана в манифесте)
- либо рекурсивными вызовами методов внутри Handler.postDelayed({}, time) друг друга внутри фрагмента или активити
Для «Охотника» проще реализовывается первый вариант.
Кастомное вью наследуется от SurfaceView(ctx, attrs). Достаточно переопределить (в случае с Котлин унаследовать) конструктор с двумя параметрами и это вью можно уже статически встраивать в макет
org.domain.packetofgame.GameView, а не создавать программно.
Помимо наследования от SurfaceView необходимо реализовать интерфейс SurfaceHolder.Callback и интерфейс Runnable. Первый интерфейс с методами, вызывающимися при создании/уничтожении вьюшечки. Второй, чтобы работать с «бесконечным» потоком, внутри которого будут меняться координаты наших объектов.
Общая схема метода run() при переопределении Runnable такова:
override fun run() {
var previousFrameTime = System.currentTimeMillis()
// isRunning = false обычно в onSurfaceDestroyed также внутри //синхронизированного блока, в остальное время isRunning == true
while(isRunning) {
try {
canvas = holder.lockCanvas() // получаем canvas для отрисовки
synchronized(holder) {
val currentTime = System.currentTimeMillis() // Получение //текущего времени
val elapsedTimeMs = currentTime – previousFrameTime // Расчет //сколько времени прошло с появления предыдущего фрейма
totalElapsedTime += elapsedTimeMs / 1000.0 // Расчет сколько // всего времени прошло с начала игры (в секундах, поэтому мс делим на 1000)
updatePositions(elapsedTimeMs * 1.0) // изменение координат
// движение всех объектов
checkIntersections() // просчет столкновений объектов друг с // другом, с краем экрана и так далее
drawGame(canvas) // Метод, отрисовывающий все объекты
previousFrameTime = currentTime
}
} finally {
holder?.unlockCanvasAndPost(canvas) // все отрисованное возвращаем главному потоку
}
}
}
3. Условие окончание игры:
Если время истекло – юзер проиграл.
Если на экране не осталось видимых уток (а оставшееся время еще больше нуля) – юзер выиграл, всё просто.
Если заряд пересекается с видимой на экране уткой, т.е. задевает утку, заставляем пропасть с экрана и заряд, и утку => попадание засчитано, оставшееся время прибавляется на 1 секунду. Проще всего рассчитывать пересечение созданием прямоугольника-«утки» Rect(duckX, duckY, duckWidth, duckHeight) и прямоугольника-«заряда» Rect(bulletX, bulletY, bulletWidth, bulletHeight) и вызовом метода intersects() класса Rect()
Например: intersect = … rectDuck.intersect(rectBullet) && duck.visible
Проигрыш: на экране остались утки, время истекло
(screen1)
Выигрыш: оставшееся время больше нуля, видимых уток на экране нет.
(screen2)
В диалоговом окне по окончанию игры может выводиться статистика (которая собирается внутри главной вьюшечки, например, количество выстрелов, общее потраченное время с учётом бонусов, количество попаданий, процент попаданий и так далее) или может просто выводиться сообщение о выигрыше / проигрыше с предложением повторить игру.
***
Пожалуйста, обратите внимание! Если у Вас есть другой вариант, примерно аналогичный с данной "игрой", можете отписать нам. Это описано лишь как пример. Нам подойдут АБСОЛЮТНО любые лёгкие игры-кликалки, без уровней и т.п.