I want to make own game using Java in Android studio. I have idea: make online pixel shooter. By the way when I added map nevermind which size I have 'laggs' like I testing with 15 FPS. I used SurfaceView to make this game. The game runs at 60 FPS, with the update() method handling game logic and the draw() method rendering the game. update()
private void update() {
// Оновлення стану прискорення
if (isAccelerated && System.currentTimeMillis() - accelerationStartTime > ACCELERATION_DURATION) {
isAccelerated = false;
playerSpeed = DEFAULT_PLAYER_SPEED;
}
// Оновлення позиції гравця
PointF direction = joystick.getDirection();
int newPlayerX = playerX + (int) (direction.x * playerSpeed);
int newPlayerY = playerY + (int) (direction.y * playerSpeed);
if (!tileMap.isWall(newPlayerX / tileMap.getTileSize(), newPlayerY / tileMap.getTileSize())) {
playerX = newPlayerX;
playerY = newPlayerY;
}
// Оновлення стрільби
if (isShooting) {
player.shoot();
}
camera.update(playerX, playerY);
}
draw()
private void draw() {
Canvas canvas = getHolder().lockCanvas();
if (canvas != null) {
try {
canvas.drawColor(0xFFFFFFFF);
tileMap.draw(canvas, camera.getX(), camera.getY());
canvas.drawBitmap(
playerBitmap,
playerX - camera.getX(),
playerY - camera.getY(),
null
);
// Малювання кулі
if (isShooting) {
canvas.drawBitmap(
bulletBitmap,
bulletX - camera.getX(),
bulletY - camera.getY(),
null
);
}
joystick.draw(canvas);
// Малювання кнопок
shootButton.draw(canvas);
accelerateButton.draw(canvas);
} finally {
getHolder().unlockCanvasAndPost(canvas);
}
}
}
The player can move using a joystick, shoot bullets, and activate a temporary speed boost. The TileMap class handles the map rendering
//TileMap.java
package coms.mempixel.game;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.util.SparseArray;
import coms.mempixel.R;
public class TileMap {
private int[][] map; // Масив, що описує карту
private Bitmap[] tileImages; // Зображення для кожного типу тайла
private int tileSize; // Розмір одного тайла (наприклад, 32x32 пікселів)
private Bitmap cachedVisibleMap; // Кешована видима частина карти
private int cachedCameraX, cachedCameraY; // Кешовані координати камери
private SparseArray<Boolean> collisionCache = new SparseArray<>();
public boolean isWall(int x, int y) {
int key = y * map[0].length + x; // Унікальний ключ для кешування
Boolean cachedResult = collisionCache.get(key);
if (cachedResult != null) {
return cachedResult;
}
boolean result;
if (y >= 0 && y < map.length && x >= 0 && x < map[y].length) {
result = map[y][x] == 1; // 1 - стіна
} else {
result = true; // Якщо координати поза межами карти, вважаємо це стіною
}
collisionCache.put(key, result);
return result;
}
public void draw(Canvas canvas, int cameraX, int cameraY) {
if (cachedVisibleMap == null || cachedCameraX != cameraX || cachedCameraY != cameraY) {
// Якщо кеш неактуальний, створюємо новий
cachedCameraX = cameraX;
cachedCameraY = cameraY;
cachedVisibleMap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
Canvas cacheCanvas = new Canvas(cachedVisibleMap);
int screenWidth = canvas.getWidth();
int screenHeight = canvas.getHeight();
int startX = Math.max(0, cameraX / tileSize);
int startY = Math.max(0, cameraY / tileSize);
int endX = Math.min(map[0].length, (cameraX + screenWidth) / tileSize + 1);
int endY = Math.min(map.length, (cameraY + screenHeight) / tileSize + 1);
for (int y = startY; y < endY; y++) {
for (int x = startX; x < endX; x++) {
int tileType = map[y][x];
if (tileType >= 0 && tileType < tileImages.length) {
cacheCanvas.drawBitmap(
tileImages[tileType],
x * tileSize - cameraX,
y * tileSize - cameraY,
null
);
}
}
}
}
// Малюємо кешовану видиму частину карти
canvas.drawBitmap(cachedVisibleMap, 0, 0, null);
}
public TileMap(Context context, int[][] map, int tileSize) {
this.map = map;
this.tileSize = tileSize;
// Завантаження зображень для тайлів
tileImages = new Bitmap[2]; // 0 - підлога, 1 - стіна
tileImages[0] = BitmapFactory.decodeResource(context.getResources(), R.drawable.floor1); // Підлога
tileImages[1] = BitmapFactory.decodeResource(context.getResources(), R.drawable.wall1); // Стіна
// Масштабування зображень до розміру тайла
for (int i = 0; i < tileImages.length; i++) {
tileImages[i] = Bitmap.createScaledBitmap(tileImages[i], tileSize, tileSize, false);
}
}
// Отримання розмірів карти в пікселях
public int getMapWidth() {
return map[0].length * tileSize;
}
public int getMapHeight() {
return map.length * tileSize;
}
// Отримання розміру тайла
public int getTileSize() {
return tileSize;
}
}
and the Camera class ensures the player stays centered on the screen.
//Camera.java
package coms.mempixel.game;
public class Camera {
private int x, y; // Позиція камери
private int screenWidth, screenHeight; // Розміри екрану
private int mapWidth, mapHeight; // Розміри карти
private float smoothSpeed = 0.1f; // Швидкість плавного переміщення
private int lastPlayerX = -1;
private int lastPlayerY = -1;
public Camera(int screenWidth, int screenHeight, int mapWidth, int mapHeight) {
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
this.mapWidth = mapWidth;
this.mapHeight = mapHeight;
}
// Оновлення позиції камери (слідкування за гравцем)
public void update(int playerX, int playerY) {
if (lastPlayerX != playerX || lastPlayerY != playerY) {
lastPlayerX = playerX;
lastPlayerY = playerY;
int targetX = playerX - screenWidth / 2;
int targetY = playerY - screenHeight / 2;
// Плавне переміщення камери
x += (targetX - x) * smoothSpeed;
y += (targetY - y) * smoothSpeed;
// Обмеження руху камери
x = Math.max(0, Math.min(x, mapWidth - screenWidth));
y = Math.max(0, Math.min(y, mapHeight - screenHeight));
}
}
// Отримання позиції камери
public int getX() {
return x;
}
public int getY() {
return y;
}
}
There main method which launch the whole code:
@Override
public void run() {
long lastTime = System.nanoTime();
double nsPerUpdate = 1000000000.0 / 60.0; // 60 FPS
double delta = 0;
while (isRunning) {
long now = System.nanoTime();
delta += (now - lastTime) / nsPerUpdate;
lastTime = now;
while (delta >= 1) {
update();
delta--;
}
draw();
}
}
My trying to speed up the code:
- Add hardware performance in manifest:
<application
android:hardwareAccelerated="true"
From all of this I tested using android profiler (View Live Telemetry) and application don't use lots of resources: Also I have pictures, but all of them have less then 20 kilobytes of memory:
So I want to share how it works (tested on much of phones and tablets):
In summary I want to share whole class of game logic:
package coms.mempixel.game;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import androidx.annotation.NonNull;
import coms.mempixel.R;
import coms.mempixel.joystick.Joystick;
import java.util.Random;
public class GameView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private Thread gameThread;
private boolean isRunning;
private Joystick joystick;
private TileMap tileMap;
private Camera camera;
private int playerX, playerY;
private int playerSpeed = 5;
private Bitmap playerBitmap;
private Bitmap bulletBitmap; // Зображення кулі
private Bitmap speedButtonBitmap;
private Bitmap shootButtonBitmap;
private boolean isAccelerated = false; // Чи активовано прискорення
private boolean isShooting = false; // Чи відбувається стрілянина
private int bulletX, bulletY; // Позиція кулі
private int bulletSpeed = 20; // Швидкість кулі
// Стани кнопок
private boolean isSpeedButtonPressed = false;
private boolean isShootButtonPressed = false;
private long accelerationStartTime = 0; // Час активації прискорення
private final long ACCELERATION_DURATION = 10000; // Тривалість прискорення (10 секунд)
// Кнопки
private Button accelerateButton;
private Button shootButton;
private Player player;
private static final int DEFAULT_PLAYER_SPEED = 5;
private static final int ACCELERATED_PLAYER_SPEED = 8;
public GameView(Context context, AttributeSet attrs) {
super(context, attrs);
getHolder().addCallback(this);
setZOrderOnTop(true);
getHolder().setFormat(PixelFormat.TRANSLUCENT);
playerBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.player);
bulletBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.bullet);
speedButtonBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.run_btn);
shootButtonBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.shoot_btn);
bulletBitmap = Bitmap.createScaledBitmap(bulletBitmap, 1, 1, false); // Зменшуємо кулю до 1x1 пікселя
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
final int[][] map = new int[100][100]; // Створюємо карту 100x100
for (int y = 0; y < 100; y++) {
for (int x = 0; x < 100; x++) {
// Визначаємо межі карти (стіни по краях)
if (x == 0 || x == 99 || y == 0 || y == 99) {
map[y][x] = 1; // Стіна
} else {
// Внутрішній простір: чергування стін та підлоги
if ((x / 10) % 2 == 0 && (y / 10) % 2 == 0) {
map[y][x] = 1; // Стіна
} else {
map[y][x] = 0; // Підлога
}
}
}
}
tileMap = new TileMap(getContext(), map, 96);
playerBitmap = Bitmap.createScaledBitmap(playerBitmap, 96, 96, false);
camera = new Camera(getWidth(), getHeight(), tileMap.getMapWidth(), tileMap.getMapHeight());
Point startPosition = getRandomFloorPosition(map);
playerX = startPosition.x * tileMap.getTileSize();
playerY = startPosition.y * tileMap.getTileSize();
joystick = new Joystick(100, getHeight() - 100, 80, 40);
// Ініціалізація кнопок
shootButton = new Button(getWidth() - 200, getHeight() - 200, 100, 50, "Shoot", shootButtonBitmap);
// Кнопка прискорення (ліворуч внизу)
accelerateButton = new Button(50, 50, 100, 50, "Accelerate", speedButtonBitmap);
isRunning = true;
gameThread = new Thread(this);
gameThread.start();
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
}
@Override
public void run() {
long lastTime = System.nanoTime();
double nsPerUpdate = 1000000000.0 / 60.0; // 60 FPS
double delta = 0;
while (isRunning) {
long now = System.nanoTime();
delta += (now - lastTime) / nsPerUpdate;
lastTime = now;
while (delta >= 1) {
update();
delta--;
}
draw();
}
}
private void update() {
// Оновлення стану прискорення
if (isAccelerated && System.currentTimeMillis() - accelerationStartTime > ACCELERATION_DURATION) {
isAccelerated = false;
playerSpeed = DEFAULT_PLAYER_SPEED;
}
// Оновлення позиції гравця
PointF direction = joystick.getDirection();
int newPlayerX = playerX + (int) (direction.x * playerSpeed);
int newPlayerY = playerY + (int) (direction.y * playerSpeed);
if (!tileMap.isWall(newPlayerX / tileMap.getTileSize(), newPlayerY / tileMap.getTileSize())) {
playerX = newPlayerX;
playerY = newPlayerY;
}
// Оновлення стрільби
if (isShooting) {
player.shoot();
}
camera.update(playerX, playerY);
}
private void draw() {
Canvas canvas = getHolder().lockCanvas();
if (canvas != null) {
try {
canvas.drawColor(0xFFFFFFFF);
tileMap.draw(canvas, camera.getX(), camera.getY());
canvas.drawBitmap(
playerBitmap,
playerX - camera.getX(),
playerY - camera.getY(),
null
);
// Малювання кулі
if (isShooting) {
canvas.drawBitmap(
bulletBitmap,
bulletX - camera.getX(),
bulletY - camera.getY(),
null
);
}
joystick.draw(canvas);
// Малювання кнопок
shootButton.draw(canvas);
accelerateButton.draw(canvas);
} finally {
getHolder().unlockCanvasAndPost(canvas);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float touchX = event.getX();
float touchY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
if (accelerateButton.isTouched(touchX, touchY)) {
playerSpeed = ACCELERATED_PLAYER_SPEED;
isSpeedButtonPressed = true;
isShootButtonPressed = false;
return true;
}
if (shootButton.isTouched(touchX, touchY)) {
isShootButtonPressed = true;
isSpeedButtonPressed = false;
return true;
}
isSpeedButtonPressed = false;
isShootButtonPressed = false;
joystick.updateHandle(touchX, touchY);
break;
case MotionEvent.ACTION_UP:
isSpeedButtonPressed = false;
isShootButtonPressed = false;
joystick.resetHandle();
break;
}
return true;
}
private Point getRandomFloorPosition(int[][] map) {
Random random = new Random();
int x, y;
// Шукаємо випадкову позицію, де є підлога (0)
do {
x = random.nextInt(map[0].length);
y = random.nextInt(map.length);
} while (map[y][x] != 0);
return new Point(x, y);
}
}