Skip to content

Tutorial

PanJohnny edited this page Sep 25, 2023 · 2 revisions

This tutorial is for java-desktop implementation

Creating your project

First you'll need to create java project with any build tool you like. I suggest maven or gradle. When you are done please add this library to the dependencies. Here is a jitpack link for this project: https://jitpack.io/#PanJohnny/PJGameLibrary. Otherwise you'll need to build it yourself. After you're done create your main class and you are set to go.

creating new project in intellij

First steps

Now when the project is fully configured we can start writing code. First I'll need to load the library.

Configuring and starting

// Initialize the library with the java desktop adaptation. Window width and height can be passed to the JDInitializer.
PJGL.init(new JDInitializer("My cool game window title!"));
final PJGL pjgl = PJGL.getInstance();
// Start!
pjgl.start();

After writing code a window should popup and following info should be logged:

[PJGL/INFO] PJGL initialized
[PJGL/INFO] PJGL started
[PJGL/INFO] Closing...

You can turn off loging, set FPS cap and more with EngineOptions. For example:

EngineOptions.logEngineState = false;

image

Here comes the player

Great we have a game but I would like to add a player.

image

Meet Arnold! I've not definetelly created it because I am to lazy to animate or draw something more complicated... On that. Let's go to something that I can control better. Coding!

public class Arnold extends GameObject {
    // Position of Arnold on screen. (in px)
    public Position position = addComponent(new Position(this, 100, 100));
    
    // Size (to hint renderer how large should Arnold be). (in px)
    public Size size = addComponent(new Size(this, 100, 100));
    
    // This component will make sure that Arnold gets rendered.
    public SpriteRenderer renderer = addComponent(new SpriteRenderer(this, "player"));
}

And look at that Arnold should be now rendered. But one key part is missing. We need to register Arnold to the game object manager or it won't know that it exists.

SpriteRegistry.registerImageSprite("player", "/arnold.png");

pjgl.getManager().addObject(new Arnold());

pjgl.start();

image

Now when arnold exists I think it should respond to the player input. Let's say it follows the mouse!

Inputs

Mouse

Movement

    @Override
    public void update(long deltaTime) {
        // Keep in super.update(deltaTime) is not needed in this scenario, we don't have any component that requires updating, but it is good practice to not to remove it.
        super.update(deltaTime);

        // Get the mouse.
        final JDMouse mouse = PJGL.getInstance().getMouse();

        // Fix the position.
        position.x = mouse.getX();
        position.y = mouse.getY();
    }

Wow now when you move the mouse Arnold moves with it. Let's say that upon left-clicking it will shoot projectile.

Clicking

image

After putting my artistic skills yet to another test I created this thing. It will serve as a projectile. So let's create a Projectile object.

public class Projectile extends GameObject {
    // Spawn out of the screen.
    public Position position = addComponent(new Position(this, -100, -100));

    public Size size = addComponent(new Size(this, 20, 20));

    public SpriteRenderer renderer = addComponent(new SpriteRenderer(this, "projectile"));

    // Collider for future use
    public Collider collider = addComponent(new Collider(this));

    private final Position.Direction direction;
    public Projectile(Arnold arnold, Position.Direction direction) {
        this.direction = direction;

        position.x = arnold.position.x;
        position.y = arnold.position.y;
    }

    @Override
    public void update(long deltaTime) {
        // In this case collider needs update to check for collisions.
        super.update(deltaTime);

        // 10 px/frame.
        final int velocity = 10;

        // Move.
        position.add(velocity * direction.x, velocity * direction.y);

        // Check if it is still in window.
        final JDWindow window = PJGL.getInstance().getWindow();

        // Remove the element if out of screen.
        if (position.x < 0 || position.x > window.getWidth() || position.y < 0 || position.y > window.getHeight()) {
            // Queue removal to prevent ConcurrentModificationException
            PJGL.getInstance().getManager().queueRemoval(this);
        }
    }
}

Now we need to detect left-click. Now let's return to Arnold's update and add this code.

position.y = mouse.getY();

if (mouse.isKeyDown(MouseAdapter.BUTTON_LEFT)) {
    // :)
    // Spawn new projectile. It will be shot to the right. (Queued to prevent concurrent modification)
    PJGL.getInstance().getManager().queueAddition(new Projectile(this, Position.Direction.RIGHT));
}

Register the sprite back in main class:

...
SpriteRegistry.registerImageSprite("projectile", "/projectile.png")

pjgl.getManager().addObject(new Arnold());

pjgl.start();

Keyboard

I want to decide in which direction should the projectile go by looking at which keys are held.

D = RIGHT

D + W = UP_RIGHT

So let's implement that, again let's go to Arthur's update. To the place where I left the :) comment.

    if (mouse.isKeyDown(MouseAdapter.BUTTON_LEFT)) {
            final JDKeyboard keyboard = PJGL.getInstance().getKeyboard();

            // Get the direction which can be combined.
            Position.Direction directionX = Position.Direction.NONE;
            Position.Direction directionY = Position.Direction.NONE;

            if (keyboard.isKeyDown('a')) {
                directionX = Position.Direction.LEFT;
            }

            if (keyboard.isKeyDown('d')) {
                if (directionX != Position.Direction.NONE) {
                    directionX = Position.Direction.NONE;
                } else
                    directionX = Position.Direction.RIGHT;
            }

            if (keyboard.isKeyDown('w')) {
                directionY = Position.Direction.UP;
            }

            if (keyboard.isKeyDown('s')) {
                if (directionY != Position.Direction.NONE) {
                    directionY = Position.Direction.NONE;
                } else
                    directionY = Position.Direction.DOWN;
            }

            Position.Direction direction = directionX.combine(directionY);

            if (direction != Position.Direction.NONE) {
                // Spawn new projectile. (Queued to prevent concurrent modification)
                PJGL.getInstance().getManager().queueAddition(new Projectile(this, direction));
            }
        }

This is how I calculated the direction and now I can shoot projectiles in diferent directions.

Collisions

To make our projectiles usefull. Let's add enemies.

image

This is Zombie.

First I'll add collider to Arnold.

public Collider collider = addComponent(new Collider(this));

Now let's implement zombie.

public class Zombie extends GameObject {
    // Spawn out of the screen.
    public Position position = addComponent(new Position(this, 300, 300));

    public Size size = addComponent(new Size(this, 60, 60));

    public SpriteRenderer renderer = addComponent(new SpriteRenderer(this, "zombie"));

    // Collider for future use
    public Collider collider = addComponent(new Collider(this));

    private final Arnold arnold;
    public Zombie(Arnold arnold) {
        this.arnold = arnold;
    }

    public Zombie(Arnold arnold, int x, int y) {
        this.arnold = arnold;
        position.x = x;
        position.y = y;
    }

    @Override
    public void update(long deltaTime) {
        // In this case collider needs update to check for collisions.
        super.update(deltaTime);

        // 3 px/frame.
        final int velocity = 2;

        // Get direction to Arnold.
        Position.Direction direction = Position.Direction.fromTo(position, arnold.position);

        // Move.
        position.add(velocity * direction.x, velocity * direction.y);

        // Zombie is colliding.
        if (!collider.getCollisions().isEmpty()) {
            if (collider.getCollisions().contains(arnold)) {
                // The zombie killed Arnold.
                System.out.println("You lost!");

                // Remove all objects
                for (GameObject o : PJGL.getInstance().getManager().getObjects()) {
                    PJGL.getInstance().getManager().queueRemoval(o);
                }

                // TODO add game over display
            } else {
                // The zombie got hit by projectile.
                PJGL.getInstance().getManager().queueRemoval(this);

                // Remove projectile / projectiles
                for (GameObject o : collider.getCollisions()) {
                    PJGL.getInstance().getManager().queueRemoval(o);
                }

                final JDWindow window = PJGL.getInstance().getWindow();
                
                // TODO add points

                Random random = new Random();

                PJGL.getInstance().getManager().queueAddition(new Zombie(arnold, 0, random.nextInt(window.getHeight())));
                if (random.nextBoolean())
                    PJGL.getInstance().getManager().queueAddition(new Zombie(arnold, window.getWidth(), random.nextInt(window.getHeight())));
            }
        }
    }
}

Zombies follow Arnold and when Arnold shoots them they have chance of duplicating - spawning randomly in the window.

The main method should look like this now in order to add first zombie:

    public static void main(String[] args) {
        // Initialize the library with the java desktop adaptation.
        PJGL.init(new JDInitializer("My cool game window title!"));
        final PJGL pjgl = PJGL.getInstance();
        SpriteRegistry.registerImageSprite("player", "/arnold.png")
        SpriteRegistry.registerImageSprite("projectile", "/projectile.png")
        SpriteRegistry.registerImageSprite("zombie", "/zombie.png")

        final Arnold arnold = new Arnold();
        pjgl.getManager().addObject(arnold);
        pjgl.getManager().addObject(new Zombie(arnold));

        // Start!
        pjgl.start();
    }

Custom rendering

Now let's say that I want to render "Game Over" string and "Points: " string. I can do that with G2DRenderer.

public class TextDisplay extends GameObject {
    public Position position = addComponent(new Position(this));
    public G2DRenderer renderer = addComponent(new G2DRenderer(this, this::render));

    public String text;
    public TextDisplay(String text, int x, int y) {
        this.text = text;
        position.x = x;
        position.y = y;
    }

    public void render(Graphics2D g) {
        // Set font to arial 20px plain.
        g.setFont(new Font("Arial", Font.PLAIN, 20));

        // x: 0, y: 0 because we don't want to move it away from the set position.
        g.drawString(text, 0, 0);
    }
}

Now after creating this, I'll add field score to Arnold.

public int score = 0;

Let's now go to Zombie where I left those TODOs.

// First TODO - display
PJGL.getInstance().getManager().queueAddition(new TextDisplay("Game Over!", 100, 100));
PJGL.getInstance().getManager().queueAddition(new TextDisplay("Final score: " + arnold.score, 100, 200));

// Second TODO - add score
arnold.score += 100;

Adding music

In order to play track I'll add this code at the end of the main class.

        try {
            Track track = new Track("/music.wav", "Industrial background - PossessedSinner", "industrial_bg");

            track.loop();
            track.setVolume(0.1f);
            track.play();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

That means the game is "finished" you can check out this version right here: https://github.com/PanJohnny/PJGLGameDemo/tree/wiki

The final polished version: https://github.com/PanJohnny/PJGLGameDemo/tree/final

NOTE: github repo contains deprecated sprite handling, always use SpriteRegistry

Screenshots

image

image

image