前言
在Java awt frame下利用画笔工具实现一个游戏框架。
该框架支持:
- 游戏动画实现
- 动图实现
- 移动实现
- 碰撞检测
- 游戏流程控制
- 游戏音乐控制
功能设计实现
①游戏动画实现
JFrame下实现画图功能
public static JFrame frame = new JFrame();
frame.setTitle("PlaneWar"); //设置窗口标题
frame.setSize(width, height); //设置窗口大小
frame.setLocationRelativeTo(null);//设置窗口居中显示
frame.setResizable(false);//设置不可调整大小
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);//设置窗口关闭时程序结束
frame.setLayout(null);
frame.setVisible(true);//设置窗口可见
frame.getGraphics().drawImage(图片,0,0,null);//把图片画出
frame.getGraphics().drawImage(图片,0,0,null);//把图片画出
上述一行代码可以把Image类在frame中显示出来。
如果采用上图方式显示图片,即
frame.getGraphics().drawImage(图片1,图片1X坐标,图片1Y坐标,null);//把图片1画出
frame.getGraphics().drawImage(图片2,图片2X坐标,图片2Y坐标,null);//把图片2画出
frame.getGraphics().drawImage(图片3,图片3X坐标,图片3Y坐标,null);//把图片3画出
这种方式画出的图片会出现频闪问题。
所以推荐下述方法②实现游戏动画实现。
将图片画在BufferImage中,然后把BufferImage画到frame中,解决游戏画面频闪问题
public BufferedImage bufferedImage = new BufferedImage(800,800,BufferedImage.TYPE_INT_RGB);
public Graphics2D bufferedImageGraphics=bufferedImage.createGraphics();
...
bufferedImageGraphics.drawImage(图片1,图片1X坐标,图片1Y坐标,null);//把图片1画出
bufferedImageGraphics.drawImage(图片2,图片2X坐标,图片2Y坐标,null);//把图片2画出
bufferedImageGraphics.drawImage(图片3,图片3X坐标,图片3Y坐标,null);//把图片3画出
...
frame.getGraphics().drawImage(bufferedImage,0,0,null);//把缓存图片画出
②动图实现
一张动图是由多张静态图片组成
将一个需要显示的类以动态图片的效果显示出来的解决方案:
- 在该类中维护一个Image容器,容器中存放组成动态图片的所有静态图片。
- 显示此类,就从容器中取一张静态图片显示
- 下一次显示此类,就从容器中取下一章静态图片显示 即可
public class BaseImage {
public int index=0;
...
public ArrayList<Image> imageList = new ArrayList<>();
...
public Image getImg(){
if(imageList.size()==0){
return null;
}else{
index=(index+1)%size;
return imageList.get(index);
}
}
}
③移动实现
游戏中的移动包含:
- 标题文字的移动
- 游戏敌人的移动
- 子弹射击的移动
移动的方向多种多样:
- 从方向a到方向b的直线移动
- 某种形状的规则移动
- 不规则移动 等等
移动作用的对象就是坐标。
用一个类记录item的移动规则,每次移动告诉这个类告诉如何修改坐标就行
public interface IMoveStrategy {
void Move(BaseImage baseImage);
}
public class BaseImage {
public int positionX;
public int positionY;
...
IMoveStrategy iMoveStrategy=null;
public void setiMoveStrategy(IMoveStrategy iMoveStrategy){this.iMoveStrategy=iMoveStrategy;}
public Image getImg(){
if(iMoveStrategy!=null)
iMoveStrategy.Move(this);
...
}
}
标题文字从下往上运动:
子弹以正方形的形状从下往上运动:
④碰撞检测
有的游戏中需要包含碰撞检测。比如子弹碰到敌人,敌人碰到游戏角色。
游戏功能设计时:
如果碰撞,则运行什么逻辑
这个时候只关心比较高层次的逻辑设计,具体如何实现碰撞并不关心。
2个物体是否碰撞与物体的具体形状有关,不同形状之间的碰撞算法是不一样的。
所以碰撞检测需要支持多种形状。
interface HitDetectionFactory{
boolean isHit(IShape shape1,IShape shape2);
}
public class HitDetection {
public boolean isHit(IShape shape1, IShape shape2){
HitDetectionFactory hitDetectionFactory =null;
if((shape1 instanceof ShapeCircle)&&(shape2 instanceof ShapeCircle)){
//圆形与圆形的碰撞检测
hitDetectionFactory=new HitDetectionCircleCircle();
}else if(((shape1 instanceof ShapeCircle)&&(shape2 instanceof ShapeRectangle))||
((shape2 instanceof ShapeCircle)&&(shape1 instanceof ShapeRectangle))){
//圆形与矩形碰撞检测
hitDetectionFactory=new HitDetectionCircleRectangel();
}//这里增加else if 可以支持多种不同形状的碰撞算法
else{//默认碰撞检测
hitDetectionFactory=new HitDetectionCircleCircle();
}
return hitDetectionFactory.isHit(shape1,shape2);
}
}
...
HitDetection ht = new HitDetection();//声明一个碰撞检测类
ht.isHit(物体1,物体2)//这里只关心物体是否碰撞,具体如何实现碰撞并不关心。
圆形与圆形的碰撞检测:
圆形与圆形的碰撞算法比较简单
比较圆形距离与2个圆形半价之和的大小即可。
对应碰撞检测的算法如下:
class HitDetectionCircleCircle implements HitDetectionFactory{
@Override
public boolean isHit(IShape shape1,IShape shape2) {
ShapeCircle s1=(ShapeCircle)shape1;
ShapeCircle s2=(ShapeCircle)shape2;
int len1=(s2.x-s1.x)*(s2.x-s1.x)+(s2.y-s1.y)*(s2.y-s1.y);
int len2=(s1.r+s2.r)*(s1.r+s2.r);
if(len1<=len2){//相撞
return true;
}
return false;
}
}
圆形与矩形碰撞检测:
圆形与矩形是否碰撞包含3种情况:
- 情况1:矩形的边角点在圆内
- 情况2:圆心到矩形中心的距离小于等于半径+矩形宽度的一半
- 情况3:圆心到矩形中心的距离小于等于半径+矩形长度的一半
对应碰撞检测的算法如下:
class HitDetectionCircleRectangel implements HitDetectionFactory{
@Override
public boolean isHit(IShape shape1, IShape shape2) {
ShapeCircle sc=null;
ShapeRectangle sr=null;
if(shape1 instanceof ShapeCircle){
sc=(ShapeCircle)shape1;
sr=(ShapeRectangle) shape2;
}else{
sc=(ShapeCircle)shape2;
sr=(ShapeRectangle) shape1;
}
if(isInTheCircle(sr.x,sr.y,sc)) return true;
if(isInTheCircle(sr.x+sr.width,sr.y,sc)) return true;
if(isInTheCircle(sr.x+sr.width,sr.y+sr.height,sc)) return true;
if(isInTheCircle(sr.x,sr.y+sr.height,sc)) return true;
int mx=sr.x+sr.width/2;
int my=sr.y+sr.height/2;
if(Math.abs(mx-sc.x)<=sc.r+sr.width/2) return true;
if(Math.abs(my-sc.y)<=sc.r+sr.height/2) return true;
return false;
}
private boolean isInTheCircle(int x,int y,ShapeCircle circle){
int len1=(circle.x-x)*(circle.x-x)+(circle.y-y)*(circle.y-y);
if(len1<=circle.r* circle.r) return true;
return false;
}
}
⑤游戏流程控制
游戏流程控制的实现比较简单:通过系统状态控制即可。
系统状态类
public class MyStation {
public static final MyStation MAIN = new MyStation();
public static final MyStation CHOICE = new MyStation();
public static final MyStation TOP = new MyStation();
public static final MyStation GAMING = new MyStation();
public static final MyStation EXIT= new MyStation();
public static boolean isReady=true;
public static MyStation curStation = MAIN;
public static void setStation(MyStation s) throws UnsupportedAudioFileException, LineUnavailableException, IOException {
curStation=s;
}
}
main函数
public static void main(String[] args) throws Exception {
frame.setTitle("PlaneWar"); //设置窗口标题
frame.setSize(width, height); //设置窗口大小
frame.setLocationRelativeTo(null);//设置窗口居中显示
frame.setResizable(false);//设置不可调整大小
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);//设置窗口关闭时程序结束
frame.setLayout(null);
frame.setVisible(true);//设置窗口可见
while(MyStation.curStation!=MyStation.EXIT){
if(MyStation.curStation==MyStation.MAIN)
new ScreenMain(frame).RunScreen();
if(MyStation.curStation==MyStation.CHOICE)
new ScreenChoice(frame).RunScreen();
if(MyStation.curStation==MyStation.TOP)
new ScreenTop(frame).RunScreen();
if(MyStation.curStation==MyStation.GAMING)
new ScreenGaming(frame).RunScreen();
}
System.exit(0);
}
⑥游戏音乐控制
java sound api(JDK)原生支 .wav .au .aiff 这些格式的音频文件,当然 PCM(Pulse Code Modulation----脉冲编码调制)文件也是可以直接播放的,如果是 mp3,ogg,ape,flac 则需要第三方 jar 。
Stream.of(AudioSystem.getAudioFileTypes()).forEach(e -> {System.out.println(e.toString());});
输出:
WAVE
AU
AIFF
单个音乐文件播放
Clip.start()方法不是阻塞操作,这意味着它不会等待,而是启动一个新的守护程序线程来播放声音,该守护程序线程在程序退出main方法后即被Kill
File musicPath = new File("路径");
AudioInputStream audioInput = AudioSystem.getAudioInputStream(musicPath);
Clip clip = AudioSystem.getClip();
clip.open(audioInput);
clip.start(); //播放音乐
clip.loop(Clip.LOOP_CONTINUOUSLY); //设置循环播放
System.out.println("播放5s");
Thread.sleep(5000);
int position=clip.getFramePosition();
clip.stop();
System.out.println("暂停2s");
Thread.sleep(2000);
clip.setFramePosition(position);
clip.start();
System.out.println("继续播放5s");
Thread.sleep(5000);
clip.close();
System.out.println("关闭");
窗体实现音乐切换
class UI{
public void initUI() throws LineUnavailableException, UnsupportedAudioFileException, IOException, InterruptedException {
JFrame jf = new JFrame("测试切换音乐");
jf.setSize(100,200);
jf.setLocationRelativeTo(null);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//设置窗体背景色
jf.getContentPane().setBackground(Color.WHITE);
jf.setLayout(new FlowLayout());
Clip clip1 = AudioSystem.getClip();
clip1.open(AudioSystem.getAudioInputStream(new File("路径1")));
clip1.loop(Clip.LOOP_CONTINUOUSLY); //设置循环播放
clip1.stop();
Clip clip2 = AudioSystem.getClip();
clip2.open(AudioSystem.getAudioInputStream(new File("路径2")));
clip2.loop(Clip.LOOP_CONTINUOUSLY); //设置循环播放
clip2.stop();
Clip clip3 = AudioSystem.getClip();
clip3.open(AudioSystem.getAudioInputStream(new File("路径3")));
clip3.loop(Clip.LOOP_CONTINUOUSLY); //设置循环播放
clip3.stop();
JButton jbu1 = new JButton("音乐1");
JButton jbu2 = new JButton("音乐2");
JButton jbu3 = new JButton("音乐3");
jbu1.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
clip1.start();
clip2.stop();
clip3.stop();
}
});
jbu2.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
clip1.stop();
clip2.start();
clip3.stop();
}
});
jbu3.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
clip1.stop();
clip2.stop();
clip3.start();
}
});
jf.add(jbu1);
jf.add(jbu2);
jf.add(jbu3);
jf.setVisible(true);
}
}
⑦动态显示数字图片
输入:整型数字+起始坐标
输出:(数字图片+坐标)的序列
public ArrayList<ImageAndMyposition> getImg(int number, int positionX, int positionY){
ArrayList<ImageAndMyposition> res= new ArrayList<>();
//System.out.println(number);
boolean flag=false;
if(number<0){
flag=true;
number=-number;
}
if(number==0){
res.add(new ImageAndMyposition(getImageN(0),new MyPosition(positionX,positionY)));
return res;
}
ArrayList<Image> numlist= new ArrayList<>();
ArrayList<MyPosition> positionlist=new ArrayList<>();
while(true){
int index=number%10;
number=number/10;
numlist.add(getImageN(index));
positionlist.add(new MyPosition(positionX,positionY));
positionX+=50;
if(number==0)break;
}
positionlist.add(new MyPosition(positionX,positionY));
if(flag)numlist.add(getImageN(10));
Collections.reverse(numlist);
for(int index=0;index<numlist.size();index++){
res.add(new ImageAndMyposition(numlist.get(index),positionlist.get(index)));
}
return res;
}
设计流程
原型设计
类图设计
多音效类图实现
新增音效直接继承BaseMusic即可
示例:
public class MusicShoot extends BaseMusic{
public MusicShoot() throws UnsupportedAudioFileException, LineUnavailableException, IOException {
clip = AudioSystem.getClip();
clip.open(AudioSystem.getAudioInputStream(new File("MusicFiles\\射击.wav")));
clip.loop(Clip.LOOP_CONTINUOUSLY); //设置循环播放
clip.stop();
}
}
图像基类实现
此项目中所有的显示效果都是通过DrawImage实现的,需要一个图像基类
imageList:ArrayList 是用来存储一张或多张Image的
素材及工具
图片素材
音乐素材
飞机大战0.5版本源码
效果
实现功能:
- 动态背景
- 角色移动
- 发射不同伤害子弹
- 随机生成敌人
- 敌人被击中效果
知识点:
- awt
- swing
- java容器
- 多线程初步实践
package game;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
public class ThreadDraw implements Runnable {
BufferedImage bi=null;
JFrame jf=null;
public static CMe me=null;
public static LinkedList<CEnemy> listEnemy=new LinkedList<>();
public static LinkedList<CBullet> listBullet=new LinkedList<>();
ThreadDraw(BufferedImage bi,JFrame jf){
this.bi=bi;
this.jf=jf;
}
@Override
public void run() {
int j=0;
Long curTime;
while(true){
try {
curTime=System.currentTimeMillis();
Thread.sleep(5);
Image img = new ImageIcon("img\\bg2.jpg").getImage();
j+=1;if(j==1000){j=0;}
//画背景
var g=bi.createGraphics();
g.drawImage(img,0,j+0,null);
g.drawImage(img,0,j-1000,null);
//画me
g.drawImage(me.getImg(),me.x,me.y,null);
//画子弹
for(int index=0;index<listBullet.size();index++){
var i =listBullet.get(index);
g.drawImage(i.getImg(),i.x,i.y,null);
i.moveYBy(-3);
if(i.y<0||i.used){
listBullet.remove(index);
index--;
}
}
//画敌人
for(int index=0;index<listEnemy.size();index++){
var i =listEnemy.get(index);
if(i.health<=0){
listEnemy.remove(index);
index--;
}
g.drawImage(i.getImg(),i.x,i.y,null);
i.moveYBy(1);
}
//检查是否碰撞
for(var e:listEnemy){
for(var b:listBullet){
e.checkHit(b);
}
}
//画出缓冲区
jf.getGraphics().drawImage(bi,0,0,null);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
//
// @Override
// public void run(){
// Long curTime;
// var g=bi.createGraphics();
// int j=0;
// int bgSpeed=-200;
//
// while(true){
// try {
// Thread.sleep(5);
// curTime=System.currentTimeMillis();
// int bgposition= (int) (bgSpeed*(UI.UIStartTime-curTime)/1000) %1000;
// Image img = new ImageIcon("img\\bg4.jpg").getImage();
// j+=1;if(j==1000){j=0;}
//
// //画背景
// g.drawImage(img,0,bgposition,null);
// g.drawImage(img,0,bgposition-1000,null);
//
// //画子弹
// for(int index=0;index<listBullet.size();index++){
// var i =listBullet.get(index);
// g.drawImage(i.getImg(),i.x,i.y,null);
// //i.moveYBy(-3);
// i.moveByTime(curTime);
// if(i.y<-50||i.used){
// listBullet.remove(index);
// index--;
// }
// }
//
// //画敌人
// for(int index=0;index<listEnemy.size();index++){
// var i =listEnemy.get(index);
// if(i.health<=0){
// listEnemy.remove(index);
// index--;
// continue;
// }
// // g.drawImage(i.getImg(),i.x,i.y,null);
// g.drawImage(i.getImg(curTime),i.x,i.y,null);
// //i.moveYBy(1);
// i.moveByTime(curTime);
// }
// //检查是否碰撞
// for(var e:listEnemy){
// for(var b:listBullet){
// e.checkHit(b);
// }
// }
//
// //画me
// g.drawImage(me.getImg(),me.x,me.y,null);
// //画缓冲区
// jf.getGraphics().drawImage(bi,0,0,null);
//
//
//
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// }
// }
}
package game;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
public class UI {
public static long UIStartTime;
public static BufferedImage bi = new BufferedImage(1000,1000,BufferedImage.TYPE_INT_RGB);
public static JFrame jf =null;
public static CMe me= null;
public static ThreadDraw td=null;
public void initUI(){
UIStartTime=System.currentTimeMillis();
jf = new JFrame("弹球游戏");
jf.setSize(1000,1000);
jf.setResizable(false);
jf.setLocationRelativeTo(null);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//jf.setLayout(new FlowLayout());
jf.setVisible(true);
td=new ThreadDraw(bi,jf);
new Thread(td).start();
me=new CMe(450,800);
ThreadDraw.me=me;
//===============监听器=====================
jf.addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent e) {
switch (e.getKeyChar()){
case 'a':
case 'A':
me.move(me.x-me.speed,me.y);
break;
case 's':
case 'S':
me.move(me.x,me.y+me.speed);
break;
case 'd':
case 'D':
me.move(me.x+me.speed,me.y);
break;
case 'w':
case 'W':
me.move(me.x,me.y-me.speed);
break;
case 'q':
case 'Q':
case '1':
ThreadDraw.listBullet.add(new CBullet());
break;
case '2':
ThreadDraw.listBullet.add(new CBullet('t'));
break;
default:
System.out.println(e.getKeyChar());
break;
}
System.out.println(e.getKeyChar());
}
});
//定时加敌人
new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
Thread.sleep(2000);
var ce=new CEnemy();
ce.x=new Random().nextInt(800)+100;
ThreadDraw.listEnemy.add(ce);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}).start();
}
public static void main(String[] args) {
// long startime= System.currentTimeMillis();
// new Thread(new Runnable() {
// @Override
// public void run() {
// while (true){
// try {
// Thread.sleep(100);
// double len=System.currentTimeMillis()-startime;
// len=len/1000;
// System.out.println(len);
// //System.out.println(System.currentTimeMillis()-startime);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// }
//
// }
// }).start();
new UI().initUI();
}
}
package game;
import javax.swing.*;
public class CBullet extends CItem{
int speedX=0;
int speedY=-200;
int damage=40;
boolean used=false;
CBullet(){
this.lastTime=System.currentTimeMillis();
img= new ImageIcon("img\\bullet.png").getImage();
damage=40;
x=UI.me.x+25;
y=UI.me.y;
}
CBullet(char type){
this.lastTime=System.currentTimeMillis();
x=UI.me.x+40;
y=UI.me.y;
switch (type){
case 't':
img = new ImageIcon("img\\雷电.png").getImage();
break;
default:
img = new ImageIcon("img\\bullet.png").getImage();
damage=60;
break;
}
}
public void moveByTime(long CurTime){
x+=speedX*(CurTime-lastTime)/1000;
y+=speedY*(CurTime-lastTime)/1000;
lastTime=CurTime;
}
}
package game;
import javax.swing.*;
import java.awt.*;
public class CEnemy extends CItem{
int health=120;
int speedX=0;
int speedY=150;
int hitcount=0;
static final int HITCOUNT=50;
static final int HITTIME=500;
long hittime=0;
Image img2= new ImageIcon("img\\enemy_hit.png").getImage();
CEnemy(){
lastTime=System.currentTimeMillis();
img= new ImageIcon("img\\enemy.png").getImage();
x=500;
y=0;
}
public void checkHit(CBullet b){
if(b.y<this.y){
if(b.x+b.img.getWidth(null)<this.x){//没中
return;
}
if(b.x>this.x+img.getWidth(null)){//没中
return;
}
//中了
hitcount=HITCOUNT;
hittime= (lastTime+HITTIME);
b.used=true;
health-=b.damage;
}
}
public void moveByTime(long CurTime){
x+=speedX*(CurTime-lastTime)/1000;
y+=speedY*(CurTime-lastTime)/1000;
lastTime=CurTime;
}
@Override
public Image getImg() {
if(hitcount>0){
hitcount--;
return img2;
}
return img;
}
public Image getImg(long time){
if(time<hittime){
return img2;
}
return img;
}
}
package game;
import java.awt.*;
public class CItem {
Image img;
int x;
int y;
CItem(){
}
// CItem(int x,int y,long lastTime){
// this.x=x;
// this.y=y;
// this.lastTime=lastTime;
// }
int speedX=0;
int speedY=1;
long lastTime;
public Image getImg(){
return img;
}
public void moveByTime(long CurTime){
x+=speedX*(CurTime-lastTime)/1000;
y+=speedY*(CurTime-lastTime)/1000;
lastTime=CurTime;
}
public void move(){
x++;
y++;
}
public void moveXBy(int len){
x+=len;
}
public void moveYBy(int len){
y+=len;
}
public void move(int x,int y){
this.x=x;
this.y=y;
}
}
package game;
import javax.swing.*;
import java.awt.*;
public class CMe extends CItem{
int speed=20;
CMe(int x,int y){
img= new ImageIcon("img\\emoji3.png").getImage();
this.x=x;
this.y=y;
}
}