☰
Java+Swing开发大鱼吃小鱼:从面向对象到游戏循环的完整实践
2026/10/8 18:28:44 网站建设 项目流程

简介:一份基于Java+Swing实现的大鱼吃小鱼小游戏完整源码包,包含用户登录与注册功能,适合Java初学者、课程设计或期末项目参考。资源围绕游戏开发的核心流程展开,涵盖窗口对象创建、背景绘制、键盘监听事件、己方鱼控制、鱼群集合管理与移动、吃鱼与被吃判定等关键环节,可帮助读者理解面向对象编程、GUI事件处理及碰撞检测的具体落地写法。压缩包共43个文件,约1.97MB,主要包含14个Java源码文件、15个class编译文件、8张png游戏素材及1张jpg背景图,并附带Eclipse工程配置与个人信息存储文本,下载解压后可直接导入IDE查看运行。已有273人学习,适合希望在完整小游戏项目中快速提升Java实战能力、梳理Swing游戏开发思路的学习者。

1. 为什么练手项目首选Java+Swing大鱼吃小鱼:从控制台到图形界面的第一道坎

学完Java基础之后最尴尬的阶段是:控制台程序写满了循环、数组和集合,但看不到像样的界面;直接去学Spring再写Web项目,又被Maven、容器、HTTP那一大堆概念淹没。这个基于Java+Swing的大鱼吃小鱼小游戏正好卡在最合适的难度上——登录注册管用户数据,界面切换管交互流程,游戏逻辑管对象与碰撞,一套代码把Java SE里最常用的语法点全用上了。对正在找java课程设计案例源码的人来说,它是能跑、能演示、能讲出设计思路的完整项目;对准备java面试题的人来说,它又是“继承多态怎么落地”“线程怎么避免并发问题”的最好素材。本文按建模、登录注册、界面绘制、避坑、进阶的顺序,给你一条可以直接复现的路线。

2. 先建模再画界面:游戏对象设计、碰撞判定与循环调度

2.1 鱼的角色体系:用继承和多态把“吃”的逻辑收敛到一处

写游戏的第一步不是画窗体,而是定数据模型。一条鱼需要哪些属性?位置x、y,体型size,游动速度speed,还有贴图和存活状态。玩家控制的鱼、随机刷新的小鱼、会反过来吃玩家的大鱼,本质上都是同一种东西——有坐标、有尺寸、会移动,差别只在“行为”不同。按面向对象编程Java的常规思路,先抽一个Fish基类,再用PlayerFish和FoodFish去继承它。

// 鱼基类:位置、尺寸、速度、贴图、是否存活 public class Fish { protected double x, y; // 鱼的中心坐标 protected int size; // 直径,决定碰撞范围和被吃关系 protected int speed; // 移动速度,单位:像素/帧 protected Image image; protected boolean alive = true; public Fish(double x, double y, int size, int speed, Image image) { this.x = x; this.y = y; this.size = size; this.speed = speed; this.image = image; } // 由子类决定具体行为;基类只做移动到目标点 public void update() { move(); } protected void move() { // 具体移动逻辑在子类里覆写 } public boolean isBiggerThan(Fish other) { return this.size > other.size; } }

这个基类把所有鱼共用的属性和“比大小”的方法收敛到了一处。size既是视觉尺寸,也是碰撞判定的核心参数,后面检测谁吃谁全靠isBiggerThan。speed用“像素/帧”而不是“像素/秒”,是因为游戏循环按帧驱动,每帧移动固定像素是Swing小游戏最常见的做法,后续调手感只需要改数字。

玩家鱼需要覆写update方法,读取键盘状态来移动:

public class PlayerFish extends Fish { // GamePanel.keys 是一个 boolean[4],分别代表上、下、左、右是否按下 public PlayerFish(double x, double y, int size, Image image) { super(x, y, size, 12, image); } @Override public void update() { int dx = 0, dy = 0; if (GamePanel.keys[0]) dy -= speed; // 上 if (GamePanel.keys[1]) dy += speed; // 下 if (GamePanel.keys[2]) dx -= speed; // 左 if (GamePanel.keys[3]) dx += speed; // 右 x += dx; y += dy; // 把鱼约束在游戏区域内,避免游出屏幕 x = Math.max(0, Math.min(GamePanel.WIDTH - size, x)); y = Math.max(0, Math.min(GamePanel.HEIGHT - size, y)); } }

这里用静态数组keys保存按键状态是图省事,正规一点应该把KeyState实例传进构造器,但课程设计场景下大家常这么写,边界清楚就行。难点在于:keys数组谁去填?答案是GamePanel里的键盘监听器。按下时置true,松开时置false,这样游戏循环每帧读取的是“持续按住的键位”,天然支持斜向移动,也不会有“只按一下只走一步”的别扭手感。面试八股里背过的继承、多态、覆写,在这里第一次变成看得见的游戏行为。

2.2 碰撞检测:为什么用半径距离而不是矩形相交

两条鱼到底吃没吃到,最直觉的想法是判断两个矩形相交——Rectangle.intersects()一行搞定。但鱼图片是圆润的,矩形判定误差很大。玩家鱼刚碰到鱼尾巴边缘就被判定吃掉,视觉上非常突兀;反过来,两条鱼视觉上已经重叠了,矩形还没相交,玩家又会觉得“明明碰到了为什么不判”。图形学里对这种圆形角色最常用的方案是圆形碰撞:两点距离小于等于两半径之和,就算碰到。

public boolean hit(Fish a, Fish b) { double r1 = a.size / 2.0; double r2 = b.size / 2.0; double dx = a.x - b.x; double dy = a.y - b.y; double distSq = dx * dx + dy * dy; double hitDist = r1 + r2 - 6; // 放宽6像素,弥补视觉误差 return distSq <= hitDist * hitDist; }

这里用距离平方比较而不是Math.sqrt,因为每秒要执行几十次、上百次的碰撞检测,能少一次开方就少一次。hitDist里减6像素是经验值:鱼的图片四周通常有透明边,数学上还差几个像素时视觉上已经碰到了,放宽一点体验更顺。不同鱼的大小差距很大时,这个值可以改成r1 + r2 - (int)(r1 * 0.1),比例放宽比固定值更稳定。

碰撞之后还要区分“谁吃谁”。玩家鱼碰到的如果是小鱼,玩家变大,小鱼死亡;碰到大鱼,玩家死亡,游戏结束。这里有个隐蔽坑:不能只判断“两鱼相撞就吃”,必须同时满足“吃的一方体型更大”,否则会出现两条鱼同时“吃掉”对方的状态,界面上一会儿你活一会儿我活。

2.3 游戏循环:Swing Timer比Thread加sleep更安全

游戏本质是一个死循环:更新坐标、检测碰撞、重绘画面。新手最容易写的版本是new Thread(() -> { while(true) { update(); repaint(); Thread.sleep(16); } }),这在Swing应用里是定时炸弹。Swing是单线程模型,所有UI操作必须在事件分派线程EDT上执行,后台线程直接调repaint()或者改组件属性,轻则界面闪烁,重则随机崩溃。

javax.swing.Timer的回调天然跑在EDT上,绕开了这个并发问题。

Timer timer = new Timer(16, e -> { updateGame(); // 玩家移动、鱼群游动 checkEat(); // 碰撞检测与“吃”逻辑 fishList.removeIf(f -> !f.alive); // 清理死亡的鱼 panel.repaint(); // 请求重绘 }); timer.start();

16毫秒约等于60FPS,鱼很多或重绘开销大的时候可以调到33毫秒,游戏依然流畅。注意Swing Timer的间隔只是“请求间隔”,实际帧率取决于EDT是否有空。如果updateGame()里塞进了文件读写、网络请求这种阻塞操作,间隔再小也白搭。这也是后面避坑章会专门讲的话题。

还有一个容易忽略的点:绘制顺序决定画面层次。分数低的鱼底层的鱼,层次顺序应该是背景->普通小鱼->大鱼->玩家鱼。给List按size排序,正好用上java排序里的Comparator:

fishList.sort(Comparator.comparingInt(Fish::getSize).reversed());

先画大、后画小,玩家鱼永远在最上层,视觉上就不会出现“小鱼把大鱼挡住”的穿帮。这条排序建议每帧重绘前都执行一次,因为鱼会变大变小,顺序一直在变。

3. 登录注册功能落地:基于文件存储的用户校验实现

3.1 存储选型:课程设计场景为什么不直接上数据库

看到“用户登录注册”,很多人的第一反应是上MySQL。但这是个单机小游戏,引入JDBC驱动、建表、写DAO,复杂度直接翻倍,还会把游戏主逻辑的戏份抢走。选择存储方案的标准很简单:数据要不要跨机器共享。单机演示、课程设计、自己练手——文件存储足够。常见做法是Properties文件或者JSON文件。Properties自带的load()/store()对中文用户名不友好,默认按ISO-8859-1处理,写进去的中文再读出来就乱码。所以更推荐自己用UTF-8按行写“用户名=密码串”的格式,解析逻辑也就三五行。以后真要扩展成联网多用户系统,把存储层换掉即可,上层接口不用动。

3.2 注册与登录完整代码:哈希加盐的UserStore工具类

以下是最小可用的用户存储工具类,复制到项目里就能跑:

public class UserStore { // 数据文件存工作目录下的 data/users.txt private static final File DB = new File("./data/users.txt"); private static final Map<String, String> users = new HashMap<>(); // 启动时加载已有用户 static { try (BufferedReader reader = new BufferedReader( new InputStreamReader(new FileInputStream(DB), StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { String[] kv = line.split("=", 2); // 只切第一个=,用户名里即使有=也不影响 if (kv.length == 2) users.put(kv[0], kv[1]); } } catch (IOException ignored) { // 首次运行文件不存在,这是正常情况 } } public static boolean register(String username, String password) { if (users.containsKey(username)) return false; String salt = UUID.randomUUID().toString().substring(0, 8); users.put(username, md5(md5(password) + salt) + ":" + salt); save(); return true; } public static boolean login(String username, String password) { String record = users.get(username); if (record == null) return false; String[] parts = record.split(":"); return parts[0].equals(md5(md5(password) + parts[1])); } private static void save() { File parent = DB.getParentFile(); if (parent != null) parent.mkdirs(); StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> e : users.entrySet()) { sb.append(e.getKey()).append("=").append(e.getValue()).append("\n"); } try (OutputStreamWriter writer = new OutputStreamWriter( new FileOutputStream(DB), StandardCharsets.UTF_8)) { writer.write(sb.toString()); } catch (IOException e) { throw new RuntimeException("用户数据保存失败", e); } } private static String md5(String src) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(src.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : bytes) sb.append(String.format("%02x", b)); return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }

这个类解决了几个关键问题。第一,密码不以明文落盘,存的是“双重MD5加盐”的结果。只存MD5不够——彩虹表能反查弱口令,加一段随机盐之后,同一密码不同用户存的值完全不同。第二,register里先containskey判重再做写入,保证了用户名唯一。第三,save()用UTF-8写,按说中文用户名就不会出现乱码;文件里每行格式是用户名=哈希:盐,split("=", 2)只切第一个等号,用户名里就算带等号也能正确解析。

有一个容易被忽略的参数:substring(0, 8)取的盐越长越安全,但文件里盐会占额外空间,单机小游戏8位足够。盐里含字母和数字,不含冒号,所以后面split(":")不会被误切。如果哪天想把注册信息加密升级成SHA-256,只需要改md5()方法内部实现,调用方完全不用动。

3.3 按钮回调与输入校验:把登录注册串进界面

有了存储层,接下来登录窗体上的按钮回调就非常薄了:

loginBtn.addActionListener(e -> { String name = userField.getText().trim(); String pwd = new String(pwdField.getPassword()); if (name.isEmpty() || pwd.isEmpty()) { JOptionPane.showMessageDialog(this, "用户名或密码不能为空"); return; } if (UserStore.login(name, pwd)) { Session.currentUser = name; JOptionPane.showMessageDialog(this, "登录成功,欢迎 " + name); // 切换到游戏面板,见4.1的CardLayout } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } });

密码框一定要用getPassword()而不是getText(),前者返回char[],用完可以手动置空,避免密码字符串残留在内存常量池里。trim()去掉用户手滑输入的首尾空格,不然“abc ”和“abc”会匹配失败。注册界面的逻辑相同:先检查两次密码输入是否一致,再调UserStore.register(),返回false时提示“用户名已存在”。

登录成功后的状态用静态类保存,省去跨界面传参的麻烦:

public class Session { public static String currentUser; public static int bestScore = 0; }

游戏结束时,把当前分数和Session.currentUser对应的历史最高分比较,如果破了纪录就调UserStore.updateScore()写回文件。updateScore的读取逻辑与3.2一致,区别只是把用户那一行的值替换掉。注意单机场景不存在并发写,不需要加文件锁;如果非要考虑“写一半断电损坏”,可以先写临时文件再renameTo覆盖原文件,这是最廉价的原子写方案。

4. 从窗体到画布:Swing布局、双缓冲绘图与图片资源加载

4.1 登录、注册、游戏三界面切换:CardLayout的正确用法

一个常见的反面写法是:登录成功后dispose()掉登录窗口,再new一个游戏窗口。这样做的后果是窗口闪烁、游戏面板从零重建,暂停后切出去再切回来,进度全丢。正确做法是用CardLayout在一个JFrame里管理多个面板,切换只是换卡片,游戏面板对象一直活着。

JFrame frame = new JFrame("大鱼吃小鱼"); CardLayout cardLayout = new CardLayout(); JPanel root = new JPanel(cardLayout); LoginPanel loginPanel = new LoginPanel(); RegisterPanel registerPanel = new RegisterPanel(); GamePanel gamePanel = new GamePanel(); root.add(loginPanel, "login"); root.add(registerPanel, "register"); root.add(gamePanel, "game"); frame.setContentPane(root); cardLayout.show(root, "login"); frame.setSize(1280, 720); frame.setLocationRelativeTo(null); // 居中 frame.setResizable(false); // 固定窗口尺寸,避免resize相关踩坑 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true);

GamePanel在这里被提前实例化了,但Swing只会对可见组件触发重绘,所以还没切过去时它不消耗绘制资源。切卡片时只需要调一句cardLayout.show(root, "login")。登录按钮的回调里,把目标卡名从"login"改成"game",整套界面切换就完成了。

这里建议setResizable(false)。对新手来说,允许拖拽窗口会带来一连串坐标适配问题:鱼跑出边界、碰撞判定偏移。固定逻辑尺寸1280x720,绘制和碰撞都按这个坐标系计算,省掉一大堆麻烦。真要支持缩放,得在componentResized事件里按比例重算所有坐标,那是进阶活,不是第一版该碰的。

4.2 paintComponent重绘与图像缩放:绘图代码为什么写在这里

游戏画面的核心在GamePanel。它继承JPanel,覆写paintComponent而不是paint。paint会先清背景再调paintComponent,直接覆写paint容易破坏Swing自带的双缓冲优化;paintComponent是Swing留给开发者填充画面的标准入口,默认双缓冲由RepaintManager管理,我们只管画。

public class GamePanel extends JPanel { private final PlayerFish player; private final List<Fish> fishList; @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 清背景 Graphics2D g2 = (Graphics2D) g.create(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 画海水背景 g2.setColor(new Color(20, 120, 180)); g2.fillRect(0, 0, getWidth(), getHeight()); // 按size排序,先画大的,再画小的,玩家鱼最后画 fishList.sort(Comparator.comparingInt(Fish::getSize).reversed()); for (Fish f : fishList) { g2.drawImage(f.image, (int) (f.x - f.size / 2.0), (int) (f.y - f.size / 2.0), f.size, f.size, this); } // 玩家鱼按移动方向旋转 AffineTransform old = g2.getTransform(); g2.rotate(Math.toRadians(player.direction), player.x, player.y); g2.drawImage(player.image, (int) (player.x - player.size / 2.0), (int) (player.y - player.size / 2.0), player.size, player.size, this); g2.setTransform(old); g2.dispose(); } }

两个细节值得记牢。第一,drawImage的最后一个参数传this作为ImageObserver,图片尚未加载完成时,Swing会收到加载完成的回调并自动触发重绘;如果传null,在高分辨率大图片的场景下,画面偶尔会白屏一瞬。第二,g2.create()先复制一份绘图上下文,最后dispose()释放,避免污染原画笔状态——比如旋转了玩家鱼后没复原,下一帧所有鱼都跟着旋转,这种诡异问题排查起来极其恶心。

抗锯齿必须开。鱼是圆形贴图,不开抗锯齿,边缘全是锯齿,整个游戏质感瞬间变成“学员作品”。

4.3 图片资源加载:ImageIO与类路径的关系

图片加载是Swing小游戏里翻车率最高的一环,没有之一。新手喜欢把图片放在项目目录里,然后用new ImageIcon("images/fish.png")引用。这个写法在IDE里能跑,但把项目打成jar包后必然找不到图片——因为images/目录不在jar包内部,路径是相对当前工作目录的,换个地方启动就崩。

正确做法是把图片放进资源目录(例如src/main/resources/images/),通过类路径读取:

// 通过类路径读取,/ 开头表示从classpath根开始查找 Image fishImage = new ImageIcon( FishRepo.class.getResource("/images/fish.png")).getImage(); // 想要更好的缩放质量,用ImageIO读取 try (InputStream in = FishRepo.class.getResourceAsStream("/images/fish.png")) { Image fishImage2 = ImageIO.read(in); }

getResource和getResourceAsStream的区别只在返回类型,一个是URL一个是流。共同点是路径前加/表示从classpath根开始找,不加/则是相对当前类所在包找。很多人把图片放错目录,运行时控制台狂刷NullPointerException,根源就在这里。

图片格式也有讲究。鱼的素材要带透明背景,必须用PNG而不是JPG。JPG没有Alpha通道,就算原图是白底,画到蓝色海水上也会带一个白框或黑框。如果手上只有JPG,要么用Photoshop抠图转PNG,要么在代码里把图片当成不透明矩形处理,丑是丑点,但不会穿帮。另外,绘制时不要每次drawImage都传大图再让Swing缩放,应该在加载阶段就把图片缩放到常用尺寸缓存起来,每帧缩放大图会明显掉帧。

5. 避坑:新手做Swing小游戏最常见的4个翻车现场

下面这几条都是做这个项目时大概率会踩的坑。每条按“现象 -> 原因 -> 解决”的顺序写,建议边写边对照。

5.1 图片变成全黑方块或者干脆不显示

现象:运行游戏后,鱼的位置上是黑色方块,或者整个界面干干净净什么都没有。黑色方块在JPG素材上尤其常见,PNG素材则表现为透明区域一片死黑。

原因:黑色块是ImageIcon在图片未加载完成时的占位表现。如果图片路径错了,加载到的内容是null,绘制时什么都不会画。透明区域变黑则是因为图片格式是JPG,没有Alpha通道,原图的白色或黑色背景被原样画了出来。

解决:素材统一用带Alpha通道的PNG。加载时用4.3节的方式——getResourceAsStream而不是磁盘路径。还有一条血泪经验:不要在IDE里测通就完事,一定打一次jar再跑,很多机器上IDE能跑是因为IDE把资源目录加进了classpath,换成直接运行就露馅。

5.2 鱼闪现残影、窗口拖拽后坐标错位、按方向键没反应

现象:鱼游动时画面闪烁像放老电影;拖拽窗口边缘后鱼跑到界面外回不来;点了登录按钮再进游戏,按键盘方向键完全没反应。

原因:三个问题三种根源。闪屏是自己开了线程循环调repaint(),与Swing的绘制线程抢资源;坐标错位是允许窗口缩放但绘制和碰撞都按固定坐标系计算;按键失灵则是点击按钮后焦点被JButton抢走,KeyListener只对获得焦点的组件生效。

解决:游戏循环一律用Timer,见2.3节;第一版直接setResizable(false),从根上规避缩放问题;焦点问题用KeyBinding代替KeyListener最稳,KeyBinding不依赖焦点:

InputMap im = gamePanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = gamePanel.getActionMap(); im.put(KeyStroke.getKeyStroke("UP"), "up"); im.put(KeyStroke.getKeyStroke("released UP"), "upRelease"); am.put("up", new AbstractAction() { public void actionPerformed(ActionEvent e) { GamePanel.keys[0] = true; } }); am.put("upRelease", new AbstractAction() { public void actionPerformed(ActionEvent e) { GamePanel.keys[0] = false; } });

WHEN_IN_FOCUSED_WINDOW意味着只要窗口在焦点中,按键就会被捕获,不再管焦点落在哪个组件上。这个改动会把“按键失灵”这个坑连根拔掉。

5.3 注册成功却登录失败,密码永远对不上

现象:注册时提示成功,文件里也能看到用户记录,但登录时永远提示“用户名或密码错误”。

原因:两种最常见的情况。第一种是编码错乱,用Properties默认编码写中文用户名,读取时解不出来,用户名key对不上;第二种是哈希拼接顺序不一致——注册时算的是md5(md5(password) + salt),登录时写的却是md5(password + salt),两个结果当然不同。

解决:把挖掘逻辑收敛到一个方法里,比如3.2节的md5(),注册和登录都调用它,不要各写各的。排查时在登录方法里临时打印期望哈希值和实际计算值,肉眼对比就能看出是编码问题还是算法问题。另外,Data文件不要用记事本反复编辑保存,Windows记事本默认带BOM头,会把用户名第一个字符前塞进不可见字符,这种问题诡异到让人怀疑人生。

5.4 游戏越玩越卡,最后控制台爆ConcurrentModificationException

现象:玩三分钟后帧率明显下降,控制台偶尔出现ConcurrentModificationException,有时候鱼会突然消失一大片。

原因:鱼死亡后直接remove出List,但可能正在遍历同一个List;或者在Timer回调里做了文件IO,每帧读盘,EDT被IO阻塞;还有一种隐蔽场景是鱼无限生成,List越来越大,重绘开销随鱼的数量线性上涨。

解决:遍历时用迭代器或removeIf,不要边遍历边remove,2.3节的removeIf(f -> !f.alive)就是安全写法。文件IO绝不在Timer回调里做,只在游戏结束时一次性写盘。鱼群数量设上限,超过阈值停止生成随机鱼:

if (fishList.size() < MAX_FISH_COUNT) { fishList.add(createRandomFish()); }

MAX_FISH_COUNT一般设为100到150,超过这个数量,玩家操作反应速度已经感知不到差别,只会白白消耗CPU。这几个坑排查完,游戏基本就稳定了。

6. 提交前最后一步:难度曲线、一键启动与验收清单

6.1 难度曲线怎么写:让鱼群密度随分数平滑上升

固定频率生成同等大小的鱼,玩三分钟就无聊了。常见做法是让生成间隔和敌方鱼尺寸跟随分数平滑变化:

int spawnInterval = Math.max(10, (int) (30.0 * Math.pow(0.99, score))); int maxEnemySize = Math.min(120, 40 + score / 50);

初始每30帧生成一条鱼,每吃一条鱼间隔乘以0.99,分数越高生成越快;敌方鱼体型上限随分数从40涨到120,但封顶,避免满屏巨物没法玩。用连续公式而不是“每10分升一级”的阶梯式难度,玩家感知不到突变,手感更自然。这两个参数直接放在GamePanel字段里,改起来一目了然。

6.2 打包运行:把游戏变成双击就能玩的jar

写完代码要给老师或面试官运行,最省事的交付物是一个jar包。用命令行打包流程如下:

javac -encoding UTF-8 --release 11 -d out src/**/*.java jar cfme game.jar MANIFEST.MF com.example.Main -C out . java -jar game.jar

--release 11会把编译API限制在Java 11范围内,比“编译通过、换机器运行报UnsupportedClassVersionError”好排查得多。MANIFEST.MF里配好Main-Class: com.example.Main。资源必须在jar内部且用类路径读取,否则换机器运行就是5.1节的黑色方块。Windows控制台乱码时,启动命令加-Dfile.encoding=UTF-8,Swing界面本身受字体影响不大,但System.out的中文会乱。

6.3 验收清单:跑通这7步再提交也不迟

清单比代码更能体现一个项目的完成度:

  1. 删除data目录后首次运行,注册一个新用户。
  2. 重启游戏,旧用户能登录,错误密码会被拒绝。
  3. 登录后进入游戏,方向键能控制鱼移动。
  4. 玩家鱼吃掉小鱼后体型变大、得分增加,碰到大鱼游戏结束。
  5. 游戏结束画面显示当前用户名与历史最高分,最高分已写回文件。
  6. 把项目打成jar,拷到另一台机器运行,图片正常、界面中文正常。
  7. 连点登录按钮、切窗口、暂停恢复,都不出异常。

我自己的习惯是:每次交这类带界面、带数据的项目之前,都会把这份清单从头到尾走一遍,尤其是换一台机器跑jar这一步——IDE里一切正常、换环境立刻露馅的情况见过太多次了。养成“拿到手的方案换个环境也能跑”的习惯,比多写两个装饰性的功能更能让你在答辩和面试里站得住。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询