用 JavaSwing 开发经典横版射击游戏:从 0 到 1 实现简易 Contra-like 游戏

横版射击游戏是很多玩家的童年回忆,其中《魂斗罗》(Contra)系列更是经典中的经典。本文将以 JavaSwing 为基础,带你一步步了解如何从无到有实现一个简易的横版射击游戏,涵盖游戏开发中的核心思路和关键技术点。

一、项目背景与目标

JavaSwing 作为 Java 自带的 GUI 工具包,虽然并非专门为游戏开发设计,但足以实现小型 2D 游戏的核心功能。本项目旨在通过复刻《魂斗罗》的基础玩法,展示如何利用 Swing 的组件体系、事件监听和绘图机制构建游戏框架。

我们的目标是实现一个包含以下功能的简易游戏:

  • 可控制的玩家角色(移动、跳跃、射击)
  • 自动行动的敌方角色
  • 滚动的游戏背景
  • 子弹碰撞检测
  • 角色生命值与战斗系统
  • 基本的游戏循环与画面刷新

二、核心组件设计思路

1. 主窗口与分层面板

游戏的基础是一个可视化窗口,我们使用 JFrame 作为主容器。由于游戏中存在背景、角色、子弹等多个元素,需要通过分层面板(JLayeredPane)实现元素的层级显示 —— 背景在最底层,角色和子弹在上方,确保视觉上的遮挡关系正确。

窗口初始化时需设置固定尺寸、禁用缩放功能,并通过 WindowAdapter 监听窗口关闭事件,确保游戏线程能正常终止。

2. 背景绘制与滚动机制

横版游戏的一大特点是背景会随角色移动而滚动,营造出探索广阔地图的感觉。我们通过自定义 BackgroundPanel 实现这一功能:

  • 加载大幅背景图片(超出窗口尺寸)
  • 维护一个水平偏移量(offsetX),通过修改偏移量控制背景显示区域
  • 当角色移动时,同步调整偏移量,使背景产生滚动效果
  • 为避免背景滚动到边缘出现空白,采用循环绘制多张背景图片的方式实现无缝滚动

3. 角色系统设计

角色是游戏的核心元素,我们采用面向对象思想设计角色体系:

  • 抽象出 Person 基类,封装所有角色的共同属性(坐标、生命值、图片资源)和行为(移动、跳跃、射击、绘制)
  • 玩家角色(Hero)和敌方角色(Enemy)继承自 Person 类,分别实现各自的特有逻辑
  • 角色移动与背景滚动联动,当角色向左右移动时,通过调整背景偏移量实现 "角色不动、场景移动" 的视觉效果,避免角色跑出屏幕

跳跃功能通过模拟重力实现:给角色一个初始向上速度,随时间逐渐减小(受重力影响),直到速度变为正(下落),最终回到初始高度。

4. 子弹系统与碰撞检测

射击是横版射击游戏的核心玩法,子弹系统设计需考虑:

  • 子弹类(Bullet)包含位置、速度、伤害值、状态(飞行中 / 爆炸)等属性
  • 实现子弹飞行逻辑:根据发射方向持续更新坐标
  • 子弹与角色的碰撞检测:通过矩形区域重叠判断(比较子弹与角色的坐标和尺寸)
  • 碰撞后处理:子弹爆炸、角色减血、播放特效(本案例通过图片切换模拟)

5. 游戏主循环

游戏的流畅运行依赖于稳定的主循环(Game Loop),我们通过单独的线程(ThreadContraL)实现:

  • 控制帧率(本案例为 60 FPS),确保画面刷新稳定
  • 每帧更新游戏状态:角色位置、子弹位置、背景偏移、AI 行为
  • 处理碰撞检测逻辑
  • 重绘界面组件,刷新视觉效果
  • 检测游戏结束条件(角色生命值为 0)

6. 输入控制

玩家通过键盘与游戏交互,我们使用 KeyListener 实现输入响应:

  • 监听方向键(A/D)控制背景滚动(角色移动)
  • 监听跳跃键(W)触发角色跳跃逻辑
  • 监听射击键(J/K)控制单发射击与连发射击
  • 监听攻击键(I)触发近战攻击动作

7. 敌方 AI 设计

简单的敌方 AI 行为能提升游戏的可玩性:

  • 移动逻辑:根据与玩家的距离随机选择前进、后退或静止
  • 攻击逻辑:在有效范围内随机发射子弹
  • 躲避行为:当玩家近战攻击时,有概率触发跳跃躲避
  • 受击反应:被击中时产生击退效果

三、开发中的关键技术点

  1. 双缓冲机制:Swing 组件默认支持双缓冲,可减少画面闪烁,确保动画流畅

  2. 线程安全:游戏线程与 Swing 事件调度线程(EDT)分离,通过 SwingUtilities.invokeLater () 确保界面更新在 EDT 中执行

  3. 图片资源加载:使用 ImageIO 和 MediaTracker 确保图片加载完成后再进行绘制,避免空指针异常

  4. 状态管理:通过布尔变量(如 isFlying、isBurst、isEscaping)维护游戏元素的状态,控制行为逻辑切换

  5. 坐标计算:所有元素的位置计算需考虑背景偏移量,确保相对位置正确

四、总结与扩展方向

通过上述设计,我们实现了一个具备基础玩法的横版射击游戏框架。这个简易版本虽然功能有限,但包含了 2D 游戏开发的核心要素:场景管理、角色控制、碰撞检测、游戏循环和用户输入。

在此基础上,还可以扩展更多功能:

  • 增加多种敌人类型和 Boss 战
  • 实现道具系统(武器升级、生命值恢复)
  • 添加关卡切换与进度保存
  • 加入音效与背景音乐
  • 优化 AI 逻辑,实现更复杂的敌方行为

JavaSwing 虽然不是游戏开发的首选工具,但通过这个案例可以看出,利用其基础组件和绘图能力,完全可以构建出具有可玩性的小型游戏。这个过程不仅能加深对 Swing 框架的理解,更能帮助开发者掌握游戏开发的基本思路和设计模式。

---------------------------------------------------------------------------------------------------------------------------------

二编:我们已经实现了音乐(BGM,击打音效)和敌人的肘击行为。

package contra.main;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;

public class BackgroundPanel extends JPanel {
    private Image backgroundImage;
    private int imageWidth;
    private int imageHeight;
    protected static int offsetX = 0; // 水平偏移量
    protected int speedX = 0; // 水平移动速度
    private final int SCROLL_SPEED = 10; // 滚动速度
    private int prevOffsetX = 0; // 上一帧的偏移量

    public BackgroundPanel() {

    }

    public BackgroundPanel(String imagePath) throws IOException {
        File imageFile = new File(imagePath);
        if (!imageFile.exists()) {
            throw new IOException("图片文件不存在: " + imagePath);
        }

        backgroundImage = ImageIO.read(imageFile);

        MediaTracker tracker = new MediaTracker(this);
        tracker.addImage(backgroundImage, 0);
        try {
            tracker.waitForAll();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException("图片加载被中断", e);
        }

        imageWidth = backgroundImage.getWidth(this);
        imageHeight = backgroundImage.getHeight(this);

        System.out.println("背景实际尺寸: " + imageWidth + "x" + imageHeight);
        setLayout(null);
    }

    public void update() {
        prevOffsetX = offsetX;
        offsetX -= speedX;
        if (offsetX < -imageWidth) {
            offsetX += imageWidth;
        } else if (offsetX >= imageWidth) {
            offsetX -= imageWidth;
        }
    }

    // 获取偏移量的变化
    public int getOffsetXChange() {
        return offsetX - prevOffsetX;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        if (backgroundImage != null) {
            g.drawImage(backgroundImage, -offsetX, 0, this);
            g.drawImage(backgroundImage, -offsetX + imageWidth, 0, this);
            g.drawImage(backgroundImage, -offsetX + (2 * imageWidth), 0, this);
        }
    }

    public void moveRight() {
        speedX = -SCROLL_SPEED;
    }

    public void moveLeft() {
        speedX = SCROLL_SPEED;
    }

    public void stopMoving() {
        speedX = 0;
    }

    public boolean canMoveRight(Person hero, Person enemy) {
        return (hero.getX() + hero.getWidth()) < enemy.getX();
    }


}package contra.main;

import javax.swing.*;

public class Bullet {
    protected int x, y;
    protected int speedFly;
    protected int hurt;
    protected boolean isFlying = false;
    protected ImageIcon imageIcon, imageBurstIcon;
    protected boolean isBurst = false;
    protected boolean needHurt = true;
    protected int countBurst = 0;

    public Bullet(int x, int y, int speedFly, int hurt, ImageIcon imageIcon, ImageIcon imageBurstIcon) {
        this.x = x;
        this.y = y;
        this.speedFly = speedFly;
        this.hurt = hurt;
        this.imageIcon = imageIcon;
        this.imageBurstIcon = imageBurstIcon;
    }

    public void fly() {
        if (isBurst) {
            countBurst++;
            needHurt = false;
        } else {
            x += speedFly;
        }
    }

    public void burstChangeImage() {
        imageIcon = imageBurstIcon;
//        System.out.println("BURST");
    }

}
package contra.main;

import javax.xml.crypto.dsig.keyinfo.KeyValue;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class ContraLListener implements KeyListener {
    private BackgroundPanel backgroundPanel;
    private Person person;
    private Enemy enemy;

    public void setBackgroundPanel(BackgroundPanel backgroundPanel) {
        this.backgroundPanel = backgroundPanel;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

    public void setEnemy(Enemy enemy) {
        this.enemy = enemy;
    }

    @Override
    public void keyTyped(KeyEvent e) {
    }

    @Override
    public void keyPressed(KeyEvent e) {
        int keyCode = e.getKeyCode();
        if (backgroundPanel != null && person != null && enemy != null) {
            if (keyCode == KeyEvent.VK_D) {
                // 只有允许移动时才执行
                if (backgroundPanel.canMoveRight(person, enemy)) {
                    backgroundPanel.moveRight();

                } else {
                    backgroundPanel.stopMoving();
                }
                ThreadContraL.isRunningAndNeedToRun = true;
            } else if (keyCode == KeyEvent.VK_A) {
                backgroundPanel.moveLeft();
                ThreadContraL.isRunningAndNeedToRun = true;
            }
            if (keyCode == KeyEvent.VK_W) {
                ThreadContraL.isJumpingT = true;
            }
            if (keyCode == KeyEvent.VK_J) {
                if (person.getPersonCondition() == 0) {
                    person.shootingSingleBullet();
                }
            }
            if (keyCode == KeyEvent.VK_K) {
                person.toggleContinuousShooting();
            }
            if (keyCode == KeyEvent.VK_I && !ThreadContraL.isJumpingT) {
                person.hitRaiseTheGunAndAttackWithTheBarrel();
                enemy.hitByBarrel(backgroundPanel.getOffsetXChange(), person);
            }
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
        int keyCode = e.getKeyCode();
        if (backgroundPanel != null) {
            if (keyCode == KeyEvent.VK_D || keyCode == KeyEvent.VK_A) {
                backgroundPanel.stopMoving();
                ThreadContraL.isRunningAndNeedToRun = false;
            }
        }
    }

}package contra.main;

import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;

public class ContraLUI {

    public final static int JPANEL_WIDTH = 1920;
    public final static int JPANEL_HEIGHT = 1075;
    public final static int INIT_HERO_X = 450;
    public final static int INIT_HERO_Y = 700;
    public final static int BACKGROUND_WIDTH = 5760;

    private ThreadContraL gameThread;

    public void initBackground() {
        try {
            JFrame jf = new JFrame("  ");
            jf.setSize(JPANEL_WIDTH, JPANEL_HEIGHT);
            System.out.println("长度  +  " + jf.getWidth() + "    宽度   +" + jf.getHeight());
            jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            jf.setLocationRelativeTo(null);
            jf.setResizable(false);

            JLayeredPane 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值