Rabu, 06 Mei 2026

JAVA - Draw Submarine 4 (Game)

 



prompt

Gemini said

Creating a full 2D game in Java Swing requires a game loop (usually via a javax.swing.Timer), key event handling, and custom painting within a JPanel.

Below is a complete, single-file implementation. You can copy this into a file named SubmarineGame.java in your NetBeans project.

SubmarineGame.java

Java
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;

public class SubmarineGame extends JFrame {
    public SubmarineGame() {
        setTitle("Submarine Strike");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setResizable(false);
        add(new GamePanel());
        pack();
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            new SubmarineGame().setVisible(true);
        });
    }
}

class GamePanel extends JPanel implements ActionListener, KeyListener {
    private Timer timer;
    private Player player;
    private ArrayList<Enemy> enemies;
    private ArrayList<Missile> playerMissiles;
    private ArrayList<Missile> enemyMissiles;
    private boolean[] keys = new boolean[256];
    private int score = 0;
    private boolean gameOver = false;
    private Random rand = new Random();

    public GamePanel() {
        setPreferredSize(new Dimension(800, 600));
        setBackground(new Color(0, 105, 148)); // Deep Sea Blue
        setFocusable(true);
        addKeyListener(this);

        player = new Player(100, 300);
        enemies = new ArrayList<>();
        playerMissiles = new ArrayList<>();
        enemyMissiles = new ArrayList<>();

        // Initialize 20 enemies
        for (int i = 0; i < 20; i++) {
            spawnEnemy();
        }

        timer = new Timer(16, this); // ~60 FPS
        timer.start();
    }

    private void spawnEnemy() {
        enemies.add(new Enemy(800 + rand.nextInt(1000), rand.nextInt(500) + 50));
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        if (gameOver) {
            g2d.setColor(Color.WHITE);
            g2d.setFont(new Font("Arial", Font.BOLD, 50));
            g2d.drawString("GAME OVER", 250, 300);
            g2d.setFont(new Font("Arial", Font.PLAIN, 20));
            g2d.drawString("Final Score: " + score, 330, 340);
            return;
        }

        player.draw(g2d);
        for (Enemy e : enemies) e.draw(g2d);
        g2d.setColor(Color.YELLOW);
        for (Missile m : playerMissiles) m.draw(g2d);
        g2d.setColor(Color.ORANGE);
        for (Missile m : enemyMissiles) m.draw(g2d);

        // UI
        g2d.setColor(Color.WHITE);
        g2d.setFont(new Font("Arial", Font.BOLD, 18));
        g2d.drawString("Score: " + score, 20, 30);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (gameOver) return;

        updatePlayer();
        updateEnemies();
        updateMissiles();
        checkCollisions();
        repaint();
    }

    private void updatePlayer() {
        if (keys[KeyEvent.VK_W]) player.y -= 4;
        if (keys[KeyEvent.VK_S]) player.y += 4;
        if (keys[KeyEvent.VK_A]) player.x -= 4;
        if (keys[KeyEvent.VK_D]) player.x += 4;
        player.updateAnimation();
    }

    private void updateEnemies() {
        for (Enemy en : enemies) {
            en.update();
            if (en.shouldFire()) {
                enemyMissiles.add(new Missile(en.x, en.y + 10, -5));
            }
        }
    }

    private void updateMissiles() {
        playerMissiles.removeIf(m -> { m.update(); return m.x > 800; });
        enemyMissiles.removeIf(m -> { m.update(); return m.x < 0; });
    }

    private void checkCollisions() {
        // Player missiles hitting enemies
        Iterator<Missile> mIt = playerMissiles.iterator();
        while (mIt.hasNext()) {
            Missile m = mIt.next();
            Iterator<Enemy> eIt = enemies.iterator();
            while (eIt.hasNext()) {
                Enemy en = eIt.next();
                if (en.getBounds().intersects(m.getBounds())) {
                    eIt.remove();
                    mIt.remove();
                    score += 100;
                    spawnEnemy(); // Keep the count at 20
                    break;
                }
            }
        }

        // Enemy missiles hitting player
        for (Missile m : enemyMissiles) {
            if (player.getBounds().intersects(m.getBounds())) {
                gameOver = true;
            }
        }
    }

    @Override
    public void keyPressed(KeyEvent e) {
        keys[e.getKeyCode()] = true;
        if (e.getKeyCode() == KeyEvent.VK_SPACE) {
            playerMissiles.add(new Missile(player.x + 40, player.y + 15, 7));
        }
    }

    @Override
    public void keyReleased(KeyEvent e) { keys[e.getKeyCode()] = false; }
    @Override public void keyTyped(KeyEvent e) {}
}

// --- Game Objects ---

class Player {
    double x, y, propAngle = 0;
    public Player(double x, double y) { this.x = x; this.y = y; }

    public void updateAnimation() { propAngle += 0.5; }

    public void draw(Graphics2D g2d) {
        AffineTransform old = g2d.getTransform();
        g2d.translate(x, y);

        // Submarine Body
        g2d.setColor(Color.GRAY);
        g2d.fill(new Ellipse2D.Double(0, 0, 50, 25));
        g2d.fill(new Rectangle2D.Double(15, -10, 15, 15)); // Conning Tower

        // Propeller (4 strokes spinning)
        g2d.setColor(Color.BLACK);
        g2d.setStroke(new BasicStroke(2));
        for (int i = 0; i < 4; i++) {
            double angle = propAngle + (i * Math.PI / 2);
            g2d.draw(new Line2D.Double(-5, 12, -5 + Math.cos(angle) * 10, 12 + Math.sin(angle) * 10));
        }
        g2d.setTransform(old);
    }

    public Rectangle2D getBounds() { return new Rectangle2D.Double(x, y, 50, 25); }
}

class Enemy {
    double x, y, startY;
    long lastFireTime;
    Random rand = new Random();

    public Enemy(double x, double y) {
        this.x = x; this.y = y; this.startY = y;
        this.lastFireTime = System.currentTimeMillis() + rand.nextInt(4000);
    }

    public void update() {
        x -= 2; // Move left
        y = startY + Math.sin(x * 0.05) * 20; // Wavy animation
        if (x < -60) x = 900; // Wrap around
    }

    public boolean shouldFire() {
        if (System.currentTimeMillis() - lastFireTime > 4000) {
            lastFireTime = System.currentTimeMillis();
            return true;
        }
        return false;
    }

    public void draw(Graphics2D g2d) {
        g2d.setColor(new Color(200, 0, 0));
        g2d.fill(new Ellipse2D.Double(x, y, 40, 20));
        g2d.fill(new Rectangle2D.Double(x + 10, y - 8, 12, 10));
        // Face left (eye)
        g2d.setColor(Color.WHITE);
        g2d.fill(new Ellipse2D.Double(x + 5, y + 5, 5, 5));
    }

    public Rectangle2D getBounds() { return new Rectangle2D.Double(x, y, 40, 20); }
}

class Missile {
    double x, y, speed;
    public Missile(double x, double y, double speed) { this.x = x; this.y = y; this.speed = speed; }
    public void update() { x += speed; }
    public void draw(Graphics2D g2d) { g2d.fill(new Ellipse2D.Double(x, y, 10, 5)); }
    public Rectangle2D getBounds() { return new Rectangle2D.Double(x, y, 10, 5); }
}

Key Technical Details:

  1. Graphics2D & Geom: I used Ellipse2D and Rectangle2D for the submarine bodies. These are more flexible than standard drawRect because they allow for precise collision checking using the .intersects() method.

  2. 4-Stroke Animation: The propeller logic in the Player class uses and to rotate four lines around a pivot point on the back of the sub.

  3. Key Polling: Instead of relying on the jerky nature of KeyListener auto-repeat, I use a boolean[] keys array to track which buttons are held down. This allows for smooth diagonal movement.

  4. Game Loop: The javax.swing.Timer runs every 16ms, triggering actionPerformed, which updates the positions of all entities and calls repaint().

  5. Enemy Logic: Enemies are stored in an ArrayList. When one is hit, it is removed and a new one is spawned off-screen to maintain the count of 20. They use a lastFireTime variable to ensure they only shoot every 4 seconds.

Tidak ada komentar: