Java多线程之赛跑游戏(含生成exe文件)
在JavaSE中,多线程是一个重要的内容。
我们要了解多线程的概念,就要先了解进程的概念;要了解进程的概念,就离不开操作系统的概念。
在一台正常运行的电脑中,计算机硬件(如CPU、内存、硬盘、网卡、显示器、键盘、鼠标等)提供了基础的硬件环境;在硬件之上,是操作系统,这是系统级的软件程序,用来管理计算机的各项软件资源,是人机交互中的关键环节;在操作系统之上,则是各类应用软件程序,比如QQ、微信、浏览器等。
进程是一个动态的概念,程序则是一个静态的概念。程序在操作系统中运行起来时,需要操作系统为其分配相应的内存、CPU等资源,成为进程。不同的进程,独享各自的内存、CPU等资源。这也是进程安全性所不可缺少的。所以,进程是操作系统分配CPU及各类系统资源的基本单位。
当进程运行起来时,通常会有多个子任务,以游戏为例,里面需要绘制游戏面板,如果游戏里有多个角色的人物进行并发活动,还需要能够获得相应的CPU资源,从而取得CPU的执行权,以进入运行状态。同时,在同一个游戏中,每个角色的有关状态数据,可以为该游戏中的其他角色所获取。由此可见,一个进程中可以有多个子任务,这些子任务需要并发轮流获取CPU的执行权,同时可以共享进程的内存资源。进程中的这些子任务就被称为线程,多个子任务就是多个线程。线程是进程中分配CPU资源的基本单位,线程不可以脱离进程而存在。
Java语言本身就是一种多线程语言。比如,在JavaSE的一个Java工程中,JVM通过main入口函数运行起来时,同时还会有GC(垃圾回收器)运行起来,以等待回收Java堆内存中变为垃圾的对象,以节省堆内存空间。
在做完上述知识铺垫和准备后,我接下来通过一个赛跑游戏,来为大家演示Java多线程的运用。
在这里,我先将该赛跑游戏的运行效果予以演示:
每次登录该游戏,会弹出一个提示框,告知用户是第几次登录,并有温馨提示。
点击“确定”或关闭该提示框,则进入游戏界面。该界面分为上下两部分,上半部分为游戏跑道,4个赛手,共4个跑道;下半部分为游戏控制面板,提示用户选择赛手,输入押注金额,以及点击“开始”按钮开始赛跑,还有“重新开始”按钮以回到比赛准备的状态等。
点击“开始”按钮后,进入赛跑游戏,游戏面板中的4个赛手开始奔跑,对于先跑到终点的赛手,会弹出提示框予以庆祝;如果用户押注失败,则用户本金会予以相应减少;如果用户押注成功,则用户本金会予以相应增加。
好了,演示完上述游戏后,接下来谈下开发思路。
首先,完成游戏前端部分。先绘制一个游戏窗体,窗体上添加面板,面板上添加各类所需的组件。
其次,游戏前端部分完成后,根据面向对象的思想,将赛手抽象为一个类,将与赛手有关的各种属性与功能均由该类实现;同时,主线程负责绘制游戏前端,各个赛手要运行时,则通过开辟子线程来实现。
再次,当赛跑游戏结束时,根据产生的第一名,刷新本金,弹出庆祝框,同时提示用户是成功或者失败;此外,将用户的本金与登录次数存入一个属性文件,用户本金与登录次数的变化会实时更新到属性文件中。
Step 1
请看游戏前端代码:
package runningGame; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Graphics; import java.awt.Image; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; import java.util.Properties; import java.util.ResourceBundle; import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; /** * 游戏 面板设计 * @author 李章勇 * 常量分析: * 4个赛手 * 赛手编号 * 胜利感言 * 跑道面板 * 4个跑道,每个跑道一张背景图 * 每个跑道上各有一个准备状态的赛手 * 终点线 * 控制面板 * 4个单选按钮[表示赛手】 * 提示选择赛手、输入押注,押注输入框 * 我的本金提示 * 开始按钮,重新开始按钮 * 多个子线程共用同一段代码,赛手类 */ public class GameJFrame extends JFrame{ //赛道背景面板 GamePanel jp_game=new GamePanel(); //4张跑道背景图 Image[] img_back=new Image[StaticIntString.NUM]; //每个跑道上处于准备状态的小人 Image[] img_ready=new Image[StaticIntString.NUM]; //每个跑道上奔跑中的小人 Image[] img_run=new Image[StaticIntString.NUM]; //游戏控制面板 JPanel jp_control=new JPanel(); //4个单选按钮 static JRadioButton[] jrb_mans=new JRadioButton[StaticIntString.NUM]; //按钮组 ButtonGroup bg=new ButtonGroup(); //提示选择姓名并输入金额 JLabel jl_inputTips=new JLabel(StaticIntString.INPUT_TIPS1); //输入押注金额 JTextField jtf_cost=new JTextField(10); JLabel jl_winFailTips=new JLabel("本次输赢:"); JLabel jl_winFail=new JLabel(StaticIntString.WIN_FAIL); //本金提示 JLabel jl_ownMoneyTips=new JLabel("我的本金:"); static Properties pro=new Properties(); //从属性文件里读取本金 static ResourceBundle resource=ResourceBundle.getBundle("runningGame.infos"); String ownMoney=resource.getString("ownMoney"); BigInteger bi_times=new BigInteger(resource.getString("times")); static BigInteger bi_bankruptcy=new BigInteger(resource.getString("bankruptcy")); String ownMoney_System=resource.getString("ownMoney_System"); //本金 JLabel jl_ownMoney=new JLabel(ownMoney); //开始按钮 JButton jbt_start=new JButton("开始"); //重新开始按钮 JButton jbt_restart=new JButton("重新开始"); static String selectedName; RunningMan[] mans=new RunningMan[StaticIntString.NUM]; public GameJFrame() { if(bi_times.toString().equals("1")){ JOptionPane.showMessageDialog(this, "首次登陆,欢迎光临!\n系统赠予您10000元,\n祝您玩得开心愉快!\n只是玩玩,不要沉溺赌博哦"); }else{ JOptionPane.showMessageDialog(this, "您这是第 "+bi_times.toString()+" 次登陆,欢迎光临!\n祝您玩得开心愉快!\n只是玩玩,不要沉溺赌博哦"); } bi_times=bi_times.add(new BigInteger("1")); try { BufferedInputStream fis=new BufferedInputStream(new FileInputStream("src//runningGame//infos.properties")); BufferedOutputStream fos=new BufferedOutputStream(new FileOutputStream("src//runningGame//infos.properties")); String ownMoney_System=resource.getString("ownMoney_System");; try { pro.load(fis); } catch (IOException e1) { e1.printStackTrace(); } pro.setProperty("times", bi_times.toString()); pro.setProperty("ownMoney", ownMoney); pro.setProperty("bankruptcy", bi_bankruptcy.toString()); pro.setProperty("ownMoney_System", ownMoney_System); try { pro.store(fos, ""); fis.close(); fos.close(); } catch (IOException e) { e.printStackTrace(); } } catch (FileNotFoundException e) { e.printStackTrace(); } for(int i=0;i<StaticIntString.NUM;i++){ img_back[i]=Toolkit.getDefaultToolkit().createImage("imgBack//"+(i+1)+".jpg"); img_ready[i]=Toolkit.getDefaultToolkit().createImage("imgReady//"+(i+1)+"2.gif"); img_run[i]=Toolkit.getDefaultToolkit().createImage("imgRun//"+(i+1)+"1.gif"); jrb_mans[i]=new JRadioButton(StaticIntString.NAMES[i]); bg.add(jrb_mans[i]); jp_control.add(jrb_mans[i]); } jbt_start.addActionListener(new ActionListener(){ boolean isSelected=false; public boolean judgeSelected(){ for(JRadioButton j:jrb_mans){ if(j.isSelected()){ isSelected=true; } } return isSelected; } @Override public void actionPerformed(ActionEvent arg0) { if(!judgeSelected()){ JOptionPane.showMessageDialog(GameJFrame.this, "您还没选人呢", "看着点", JOptionPane.WARNING_MESSAGE); return; } if(jtf_cost.getText().trim().length()==0){ JOptionPane.showMessageDialog(GameJFrame.this, "您还没下注呢", "看着点", JOptionPane.WARNING_MESSAGE); return; } char[] cs=jtf_cost.getText().toCharArray(); for(int i=0;i<cs.length;i++){ if(cs[i]<'0' || cs[i]>'9'){ JOptionPane.showMessageDialog(GameJFrame.this, "您输入的不是数字", "看着点", JOptionPane.WARNING_MESSAGE); jtf_cost.setText(""); return; } if(cs[0]=='0'){ JOptionPane.showMessageDialog(GameJFrame.this, "数字首位不能是0", "看着点", JOptionPane.WARNING_MESSAGE); jtf_cost.setText(""); return; } } BigInteger bi_jtf_cost=new BigInteger(jtf_cost.getText()); BigInteger bi_jl_ownMoney=new BigInteger(jl_ownMoney.getText()); if((bi_jtf_cost.compareTo(bi_jl_ownMoney))>0){ JOptionPane.showMessageDialog(GameJFrame.this, "您的下注金额不能超过本金哦", "看着点", JOptionPane.WARNING_MESSAGE); jtf_cost.setText(""); return; } for(JRadioButton j:jrb_mans){ if(j.isSelected()){ selectedName=j.getText(); break; } } for(JRadioButton j:jrb_mans){ j.setEnabled(false); } jtf_cost.setEditable(false); jl_inputTips.setText(StaticIntString.INPUT_TIPS2); jbt_restart.setEnabled(true); jbt_start.setEnabled(false); RunningMan.isEnd=false; RunningMan.canRun=true; RunningMan.num=0; for(int i=0;i<StaticIntString.NUM;i++){ mans[i].img=img_run[i]; mans[i].start(); } } }); jbt_restart.addActionListener(new ActionListener(){ @Override public void actionPerformed(ActionEvent arg0) { for(int i=0;i<StaticIntString.NUM;i++){ jrb_mans[i].setText(StaticIntString.NAMES[i]); jrb_mans[i].setEnabled(true); } jl_inputTips.setText(StaticIntString.INPUT_TIPS1); jtf_cost.setText(""); jtf_cost.setEditable(true); jl_winFail.setForeground(Color.BLACK); jl_winFail.setText(StaticIntString.WIN_FAIL); jbt_start.setEnabled(true); jbt_restart.setEnabled(false); RunningMan.isEnd=true; RunningMan.canRun=false; bg.clearSelection(); for(int i=0;i<StaticIntString.NUM;i++){ mans[i]=new RunningMan(StaticIntString.NAMES[i], 0, (jp_game.getHeight()*i)/4+30, img_ready[i], GameJFrame.this); } } }); jp_control.setBorder(BorderFactory.createTitledBorder("游戏控制系统")); jp_control.add(jl_inputTips); jp_control.add(jtf_cost); jp_control.add(jl_winFailTips); jp_control.add(jl_winFail); jp_control.add(jl_ownMoneyTips); jp_control.add(jl_ownMoney); jp_control.add(jbt_start); jp_control.add(jbt_restart); this.add(jp_game,BorderLayout.CENTER); this.add(jp_control, BorderLayout.SOUTH); setResizable(false); setTitle("赛跑游戏 ---作者:李章勇"); setBounds(200, 100, 1000, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setVisible(true); for(int i=0;i<StaticIntString.NUM;i++){ mans[i]=new RunningMan(StaticIntString.NAMES[i], 0, (jp_game.getHeight()*i)/4+30, img_ready[i], this); } } class GamePanel extends JPanel{ @Override public void paint(Graphics g) { super.paint(g); for(int i=0;i<StaticIntString.NUM;i++){ g.drawImage(img_back[i], 0, (this.getHeight()*i)/4,this.getWidth(),this.getHeight()/4+30,this); mans[i].drawSelf(g, this); } g.setColor(Color.RED); g.fillRect(850, 0, 5, this.getHeight()); } } public static void main(String[] args) { new GameJFrame(); } }
Step 2
将赛手抽象为一个类,同时创建一个接口工具,存储常数,便于对数据的统一管理;再创建一个名为“infos.properties”的属性文件,将用户本金、登录次数等信息存储到属性文件里,以备更新。
package runningGame;
public interface StaticIntString {
public static final int NUM=4;
public static final String[] NAMES={ "1号选手", "2号选手", "3号选手", "4号选手" };
public static final String[] WINS={ "预测未来的最好方法,就是创造未来.", "人生重要的不是所站的位置,而是所朝的方向.",
"如你想要拥有完美无暇的友谊,可能一辈子找不到兄弟姐妹","正因年轻咱们一无所有,也正正因年轻咱们将拥有一切." };
public static final String INPUT_TIPS1="请选择赛手并输入金额:";
public static final String INPUT_TIPS2="本次下注金额:";
public static final String GET_WIN="[获胜]";
public static final String WIN_FAIL="[Win/Fail]";
public static final String WIN_STATE="[Win]";
public static final String FAIL_STATE="[Fail]";
}
属性文件infos.properties:
#破产次数,登录次数,系统默认赠予的本金,用户的实际本金;只要用户本金小于等于0,系统会自动为用户赠予初始本金。 #Fri Dec 01 09:34:34 CST 2017 bankruptcy=0 times=1 ownMoney_System=10000 ownMoney=10000
赛手类RunningMan:
package runningGame; import java.awt.Graphics; import java.awt.Image; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.math.BigInteger; import java.util.Random; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; /** * 赛手的属性:姓名,坐标(x/y),自身图像,准备状态的图像,关联的窗体 * 位移所需的随机数Random, * 第一名NO1 * 是否能跑的状态canRun * 比赛是否结束isEnd * 每个赛手跑完时计数num * * 赛手画自己的方法drawSelf * 赛手跑步位移的方法move * 子线程运行的run方法 * * @author 李章勇 * */ public class RunningMan extends Thread { public String name; private int x_point,y_poinit; public Image img; private Image imgReady; private GameJFrame jf_game; static Random rand=new Random(); static RunningMan NO1; static boolean canRun=true; static boolean isEnd=false; static int num=0; static BigInteger bi_ownMoney; public RunningMan(String name,int x_point,int y_point,Image img,GameJFrame jf_game) { this.name=name; this.x_point=x_point; this.y_poinit=y_point; this.img=img; this.imgReady=img; this.jf_game=jf_game; } public void setName2(String name){ this.name=name; } public String getName2(){ return name; } public void drawSelf(Graphics g,JPanel jp){ g.drawImage(img, x_point, y_poinit, jp); } public void move(){ x_point+=10+rand.nextInt(30); } @Override public void run() { while(x_point<=850 && canRun){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } move(); } synchronized(String.class){ if(canRun){ x_point=850; img=imgReady; num++; } if(!isEnd){ NO1=this; isEnd=true; } if(num==4){ System.out.println("本次比赛第一名:"+NO1.name); //根据比赛结果,修改金额 类 bi_ownMoney=new GetResult(jf_game).getRightMoney(); new WinDialog(); try { BufferedInputStream fis = new BufferedInputStream(new FileInputStream("src//runningGame//infos.properties")); BufferedOutputStream fos=new BufferedOutputStream(new FileOutputStream("src//runningGame//infos.properties")); try { GameJFrame.pro.load(fis); if((bi_ownMoney.compareTo(new BigInteger("0"))<=0)){ JOptionPane.showMessageDialog(jf_game, "您已没有本金,\n系统不差钱儿,再次赠予您 10000 元!\n祝您玩得开心哦!", "看着点", JOptionPane.WARNING_MESSAGE); jf_game.jl_ownMoney.setText(GameJFrame.resource.getString("ownMoney_System")); GameJFrame.pro.setProperty("ownMoney", GameJFrame.resource.getString("ownMoney_System")); BigInteger bi_bankruptcy=new BigInteger(GameJFrame.resource.getString("bankruptcy")).add(new BigInteger("1")); GameJFrame.pro.setProperty("bankruptcy", bi_bankruptcy.toString()); }else{ jf_game.jl_ownMoney.setText(bi_ownMoney.toString()); GameJFrame.pro.setProperty("ownMoney", bi_ownMoney.toString()); } GameJFrame.pro.store(fos, ""); fis.close(); fos.close(); } catch (IOException e1) { e1.printStackTrace(); } } catch (FileNotFoundException e1) { e1.printStackTrace(); } for(JRadioButton j:GameJFrame.jrb_mans){ if(NO1.name.equals(j.getText())){ j.setText(j.getText()+StaticIntString.GET_WIN); } } } } } }
刷新用户本金的类:
package runningGame; import java.awt.Color; import java.io.BufferedOutputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.math.BigInteger; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JRadioButton; import javax.swing.JTextField; /** * 需要的属性 * 代表四个赛手的按钮 * 输入的金额 * 输赢状态 * 本金 * 关联的窗体 * 获得比赛后、发生变化的钱数getRightMoney * 设置本金新的数值setNewMoney * @author 李章勇 * */ public class GetResult { private JRadioButton[] jrb_mans; private JTextField jtf_cost; private JLabel jl_winFail; private JLabel jl_ownMoney; public GetResult(GameJFrame jf_game) { this.jrb_mans=jf_game.jrb_mans; this.jtf_cost=jf_game.jtf_cost; this.jl_winFail=jf_game.jl_winFail; this.jl_ownMoney=jf_game.jl_ownMoney; } public BigInteger getRightMoney(){ BigInteger bi_ownMoney=new BigInteger(jl_ownMoney.getText()); BigInteger bi_cost=new BigInteger(jtf_cost.getText()); if(GameJFrame.selectedName!=null && GameJFrame.selectedName.equals(RunningMan.NO1.name)){ jl_winFail.setForeground(Color.GREEN); jl_winFail.setText(StaticIntString.WIN_STATE); bi_ownMoney=bi_ownMoney.add(bi_cost); jl_ownMoney.setText(bi_ownMoney.toString()); }else{ jl_winFail.setForeground(Color.RED); jl_winFail.setText(StaticIntString.FAIL_STATE); bi_ownMoney=bi_ownMoney.subtract(bi_cost); jl_ownMoney.setText(bi_ownMoney.toString()); } return bi_ownMoney; } }
弹出庆祝框的类WinDialog:
package runningGame; import java.awt.Color; import java.awt.Graphics; import java.awt.Image; import java.awt.Toolkit; import java.util.Random; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JPanel; /** * 对话框背景图 * 4个赛手获胜时的图片 * 自定义面板 * 随机数,随机获取胜利宣言 * @author 李章勇 * */ public class WinDialog extends JDialog{ Image img_back=Toolkit.getDefaultToolkit().createImage("imgWin//winbg.jpg"); Image[] img_wins=new Image[StaticIntString.NUM]; WinJPanel wp=new WinJPanel(); Random rand=new Random(); int num=0; public WinDialog() { for(int i=0;i<img_wins.length;i++){ img_wins[i]=Toolkit.getDefaultToolkit().createImage("imgWin//"+(i+1)+"3.gif"); } wp.setBounds(100, 100, 400, 300); add(wp); setTitle("胜利墙"); setBounds(300, 300, 400, 300); setResizable(false); setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); setVisible(true); num=rand.nextInt(4); } class WinJPanel extends JPanel{ @Override public void paint(Graphics g) { super.paint(g); g.drawImage(img_back, 0, 0, this.getWidth(), this.getHeight(), this); switch(RunningMan.NO1.name){ case "1号选手": g.drawImage(img_wins[0], 50, 100, this); break; case "2号选手": g.drawImage(img_wins[1], 50, 100, this); break; case "3号选手": g.drawImage(img_wins[2], 50, 100, this); break; case "4号选手": g.drawImage(img_wins[3], 50, 100, this); break; default: break; } g.setColor(Color.RED); g.drawString("本次获胜选手:"+RunningMan.NO1.name, 20, 30); g.drawString(StaticIntString.WINS[num], 20, 50); } } }
到此,代码部分就结束了。至于代码中用到的图片,读者参考下述对图片的命名规则,收集相应的图片即可。
到此,整个单机赛跑游戏是完成了。然而,目前在运行时,我们需要在Eclipse里运行项目才可以打开游戏,显然这种方式并不符合普通用户。为此,我们来看如何把项目打包成jar包后,再生成exe可执行文件,这样的话,用户只要双击运行该exe可执行文件,就可以打开游戏。
在此,介绍一款将jar文件生成exe文件时用到的第三方软件:exe4j(下载地址:http://www.ej-technologies.com/download/exe4j/files):
至于接下来应该怎样把jar包生成exe文件,有一篇博客《将java项目打包成jar包并生成可独立执行的exe文件》中已经介绍得很详细,建议读者参考。
现在,一个完整的exe版单机赛跑游戏就制作好了。