只用2小时,开发足球射门游戏
游戏界面实现思路&代码
1)界面术语定义
2)观众区绘制思路&代码
观众区用swing代码进行实现,绘制的思路如下:绘制背景矩形框;绘制次排观众——首先,绘制一个圆和椭圆合并成一个观众样式。然后,绘制一个小一点的圆和椭圆合并成一个观众样式。最后,组合并放置到第2排位置,并铺满整排;绘制首排观众【与上一步一致,注意换观众的颜色】——首先,绘制一个圆和椭圆合并成一个观众样式;然后,绘制一个小一点的圆和椭圆合并成一个观众样式;最后,组合并放置到第1排位置,并铺满整排;绘制两个不同颜色的矩形框,并摆放最前面遮挡第1排观众的“脚”;绘制几根黑线,遮挡广告边缘。
通过swing中的Graphics2D对象绘制第二排观众:
// 观众背景
g2d.setColor(personBgColor);
g2d.fillRect(0,y,getWidth(),100);//画矩形
for (int i = 0; i < getWidth(); i+=46) {
// 观众第二排
g2d.setColor(person2);
g2d.fillOval(i,36,16,16);//画圆
g2d.fillArc(i-7,50,30,50,0,180);// 画扇行
g2d.fillOval(i+24,40,13,13);
g2d.fillArc(i+18,51,24,49,0,180);
}
通过swing中的Graphics2D对象绘制第一排观众:
// 观众第一排
y+=90;
g2d.fillRect(0,y-30,getWidth(),50);
g2d.setColor(person1);
for (int i = 0; i < getWidth(); i+=46) {
g2d.fillOval(i+7,y-41,16,16);
g2d.fillArc(i,y-25,30,50,0,180);
g2d.fillOval(i+31,y-34,13,13);
g2d.fillArc(i+25,y-24,24,49,0,180);
}
通过swing中的Graphics2D对象绘制广告栏和黑线:
// 绘制广告栏和黑线
g2d.setColor(personBgColor1);
g2d.fillRect(0,y,getWidth(),20);
g2d.setColor(personLineColor);
g2d.fillRect(0,y,getWidth(),2);
y+=20;
g2d.setColor(personBgColor2);
g2d.fillRect(0,y,getWidth(),18);
y+=18;
g2d.setColor(personLineColor);
g2d.fillRect(0,y,getWidth(),2);
for (int i = 1; i < 5; i++) {
g2d.fillRect(getWidth()*i/5,y-38,2,38);
}
3)草坪绘制思路&代码
参考实现代码:
// 足球草地
int count =0;
int h1=60;
for (int i = y; i < getHeight(); i++) {
if(count%2==0){
g2d.setColor(bgColor1);
}else{
g2d.setColor(bgColor2);
}
g2d.fillRect(0,y,getWidth(),h1+count*10);
y+=h1+count*10;
count++;
}
4)禁区三维绘制思路&代码
禁区用swing代码进行实现,绘制的思路如下:绘制一个空心的圆角矩形;使用变换技术让圆角矩形具有三维立体效果;调整禁区框的位置;用一个绿色矩形遮挡多余的禁区部分。
动画呈现如下:
通过swing中的Graphics2D对象绘制两个圆角矩形合并:
g2d.setColor(Color.WHITE);
g2d.fillRect(0,175,getWidth(),5);
AffineTransform oldTx = g2d.getTransform();
Stroke stroke = new BasicStroke(5);
g2d.setStroke(stroke);
AffineTransform tx = new AffineTransform();
tx.setToShear(-0.5, 0);
g2d.setTransform(tx);
g2d.drawRoundRect(getWidth()*5/20,175,getWidth()*6/10,120,10,10);
AffineTransform tx2= new AffineTransform();
tx2.setToShear(0.5, 0);
g2d.setTransform(tx2);
g2d.drawRoundRect(getWidth()*3/20,175,getWidth()*6/10,120,10,10);
g2d.setTransform(oldTx);
擦除中间不要的部分:
//清理内部多余的线
g2d.setColor(bgColor1);
g2d.fillRect(0,173,getWidth(),2);
g2d.fillRect(getWidth()*3/20+10,180,getWidth()*7/10-20,20);
g2d.fillRect(getWidth()*3/20+10,270,getWidth()*6/10-20,23);
g2d.setColor(bgColor2);
g2d.fillRect(getWidth()*3/20+10,200,getWidth()*6/10-20,70);
5)球门球网三维绘制思路&代码
球门与球网的绘制是最难的,卡点在于如何利用平面技术实现三维效果,绘制的思路如下:绘制一个圆角矩形,作为球门框;绘制两条弯折折线,作为球门内门柱;绘制多条水平的三线段折线,作为球门水平球门线;绘制多条垂直的二线段折线,作为球门垂直球门线。
定义对应的尺寸大小,绘制球网竖线:
g2d.setColor(Color.WHITE);
int step = 15;
int startX = getWidth()*4/20+20;
int centerX = getWidth()/2;
int startY = 85;
// 球网竖线
for (int i = startX; i < (getWidth()*4/5); i=startX) {
int x[] = {startX,startX+(startX<centerX?+8:-8),startX+(startX<centerX?+12:-12)};
int y[] = {startY,startY+10,155};
if((centerX-startX)!=-10) {
g2d.drawPolyline(x, y, x.length);//画折线
}
startX+=step;
}
实现球门内柱偏移:
// 球迷柱左里
Stroke stroke = new BasicStroke(5);
g2d.setStroke(stroke);
startX = getWidth()*4/20+20;
int tempX[] = {startX-18,startX-2,startX-2};
int tempY[] = {startY+3,startY+10,153};
g2d.drawPolyline(tempX, tempY, tempY.length);
startX = getWidth()*4/5;
int rightX[] = {startX-2,startX-20,startX-24};
int rightY[] = {startY+3,startY+10,153};
g2d.drawPolyline(rightX, rightY, rightY.length);
stroke = new BasicStroke(1);
g2d.setStroke(stroke);
startX = getWidth()*4/20;
通过swing中的Graphics2D对象绘制球网横线:
// 球网横线
int endX = getWidth()*4/5;
for (int i = startY; i < 156; i=startY) {
int x[] = {startX,startX+(startX<centerX?+18:-18),endX-12,endX};
int y[] = {startY,startY-3,startY-3,startY};
g2d.drawPolyline(x,y,x.length);
startY+=step;
}
int x[] = {startX,startX+(startX<centerX?+18:-18),endX-12,endX};
int y[] = {158,155,155,158};
g2d.drawPolyline(x,y,x.length);
通过swing中的Graphics2D对象绘制门框:
// 绘制门框
g2d.setColor(doorColor);
stroke = new BasicStroke(9);
g2d.setStroke(stroke);
g2d.drawRoundRect(getWidth()*4/20,85,getWidth()*3/5,95,20,20);// 带有圆角的矩形
stroke = new BasicStroke(5);
g2d.setStroke(stroke);
g2d.setColor(Color.WHITE);
g2d.drawRoundRect(getWidth()*4/20,86,getWidth()*3/5,95,20,20);
g2d.fillRect(0,175,getWidth(),5);
6)射门区绘制思路&代码
射门区用swing代码进行实现,绘制的思路如下:绘制两种不同颜色的扇形;叠加两个扇形;配上文字。
参考实现代码:
// 蓄力区
g2d.setColor(Color.WHITE);
g2d.fillArc((getWidth()-64)/2-80,460,224,160,0,180);
g2d.setColor(Color.ORANGE);
g2d.fillArc((getWidth()-64)/2-80,460,224,160,0,arc);
// 文字
Font font = new Font("阿里巴巴普惠体 Medium",1,24);
g2d.setFont(font);
g2d.setColor(Color.WHITE);
g2d.drawString("Ctrl + 鼠标:移动球",30,400);
g2d.drawString("拖动鼠标:调整角度",50,440);
g2d.drawString("点击鼠标:射门",50,480);
7)计分区绘制思路&代码
计分区用swing代码进行实现,绘制的思路如下:绘制两种不同颜色的矩形;用两种不同颜色的矩形铺满整个屏幕;调整矩形的高度是从上下一次递增,呈现立体视觉。
参考实现代码:
Graphics2D g2d = (Graphics2D)g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(bgColor1);
g.fillRect(0,0,getWidth(),getHeight());
g.setColor(bgColor2);
g.fillRect(0,8,getWidth(),getHeight()-8);
Font font = new Font("阿里巴巴普惠体 Medium",1,32);
g.setFont(font);
g2d.setColor(Color.BLACK);
g2d.drawString("分数",30,45);
g2d.drawString("TIME",250,45);
g2d.drawString("次数",540,45);
g2d.setColor(textColor);
g2d.fillRoundRect(100,20,80,30,25,25);
g2d.fillRoundRect(335,20,120,30,25,25);
g2d.fillRoundRect(620,20,80,30,25,25);
g2d.setColor(Color.WHITE);
g2d.drawString(String.format("%02d",score),120,47);
g2d.drawString(String.format("%02d:%02d",time/60,time%60),345,47);
g2d.drawString(String.format("%02d",count),638,47);
8)守门员&球绘制思路&代码
守门员、球绘与石头这里涉及比较多复杂细节,利用图片代替代码绘制,实现的思路如下:加载图片;把图片摆放到对应位置。
编写通过图片导入实现绘制球的代码:
// 球
public class Ball extends JLabel{
int startX = 0 ;
int startY = 0;
public Ball(){
this.setPreferredSize(new Dimension(64,64));
this.setIcon(new ImageIcon(ResourcesUtil.getRootPath()+"\\ball\\football.png"));// 加载图片
}
}
// 守门员
public Goalkeeper(BackgroundPanel backgroundPanel){
this.backgroundPanel = backgroundPanel;
this.setBounds(backgroundPanel.getWidth()/2,100,78,128);
this.setPreferredSize(new Dimension(78,128));
this.setIcon(new ImageIcon(ResourcesUtil.getRootPath()+"\\ball\\smy.png"));
}
编写通过图片导入实现绘制石头的代码:
// 石头
public class Shitou extends JLabel implements Obstacle {
BackgroundPanel backgroundPanel;
public Shitou(BackgroundPanel backgroundPanel){
this.backgroundPanel = backgroundPanel;
this.setBounds(backgroundPanel.getWidth()/2+50,100,316,100);//设置图片放置的位置
this.setPreferredSize(new Dimension(316,100));
this.setIcon(new ImageIcon(ResourcesUtil.getRootPath()+"\\ball\\st.png"));
}
@Override
public String name() {
return "石头";
}
@Override
public JComponent getComponent() {
return this;
}
public void start(){
}
public void stop(){
}
}
如何实现游戏逻辑
轨迹实现的思路如下:假设有两点,黄色点为足球的中心点,红色点是球门的中心点;在黄点和红点之间就存在一条红色线段;在红色线段上随机取N个点,用白色表示,这样就形成了一个直线的轨迹点。
接着再说一下拖动鼠标,轨迹跟着鼠标移动的实现思路:鼠标向上拖动,黄点和红点同步向上平移,这样线段上的轨迹点也同步平移;鼠标向下拖动,黄点和红点同步向下平移,这样线段上的轨迹点也同步平移;鼠标向右拖动,黄点和红点同步向右平移,这样线段上的轨迹点也同步平移;鼠标向左拖动,黄点和红点同步向左平移,这样线段上的轨迹点也同步平移。
注意,黄点和红点之间如果是曲线,效果更贴近自然,所以最后还需要把黄点和红点之间使用二次曲线进行实现。
记录起始点与终点:
// 记录红黄点
public void reDraw(Ball ball,BackgroundPanel backgroundPanel,int stepX,int stepY,boolean isControlDown){
// 开始点——黄点
startY = ball.getY()+ball.getHeight()/2;
startX = ball.getX()+ball.getWidth()/2;
// 结束点——红点
endX = getWidth()/2+stepX/3;
endY = 0;
}
// 画出轨迹线
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.RED);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
// 二次曲线
QuadCurve2D quadCurve2D = new QuadCurve2D.Double(startX,startY, startX+stepX,endY+stepY+50, endX, endY+stepY);
// 通过二次曲线,随机生成线上的几个点
PathIterator pi = quadCurve2D.getPathIterator(g2d.getTransform(),6);// 从二次曲线中取出特征点
points = new ArrayList<>(25);
while (!pi.isDone()) {
double[] coords = new double[6];
switch (pi.currentSegment(coords)) {
case PathIterator.SEG_MOVETO:
case PathIterator.SEG_LINETO:
points.add(new Point2D.Double(coords[0], coords[1]));
break;
}
pi.next();
}
// 在每个点 画上小白圆圈
g2d.setColor(Color.WHITE);
Point2D.Double point = null;
for (Point2D.Double temp : points) {
point = temp;
g2d.fillOval((int) point.x, (int) point.y,10,10);//在每个特征点上画一个小圆圈
}
}
ball.addMouseMotionListener(new MouseAdapter() {
public void mouseDragged(MouseEvent e){
// 记录拖动最后的坐标点,用于记录拖动平移的差量
int stepX = e.getX();
int stepY = e.getY();
line.reDraw(ball,BackgroundPanel.this,stepX,stepY,e.isControlDown());
repaint();
}
});
2)拖动球,摆放球的逻辑实现
public void reDraw(Ball ball,BackgroundPanel backgroundPanel,int stepX,int stepY,boolean isControlDown){
// 移动最后的位置点
this.stepX = stepX;
this.stepY = stepY;
// 按下Ctrl键拖动才是,摆放
if(isControlDown) {
this.setVisible(false);
ball.setBounds(ball.getX() + stepX, ball.getY(), ball.getWidth(), ball.getHeight());
}
}
3)射门,球按照轨迹飞行实现
参考实现代码:
// 监听点击球
ball.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
// 用一个线程让球按照瞄准轨迹飞行
new Thread(new Runnable() {
@Override
public void run() {
int star =0;
// 获取到轨迹线上的轨迹点
for (Point2D.Double point : line.getPoints()) {
// 把球移动到轨迹点上
ball.setBounds((int) point.x, (int) point.y,ball.getWidth(),ball.getHeight());
try {
// 休眠100毫秒
Thread.sleep(100);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}).start();
}
});
4)消除轨迹点重合的星星逻辑实现
参考实现代码:
ball.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
new Thread(new Runnable() {
@Override
public void run() {
int star =0;
for (Point2D.Double point : line.getPoints()) {
ball.setBounds((int) point.x, (int) point.y,ball.getWidth(),ball.getHeight());
try {
// 球在飞行过程中,没移动一个点,需要检查一下是否有星星需要消除
star += obstacleStart();
Thread.sleep(100);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
check(star);
}
}).start();
CLICK_CLIP.play();
}
});
public int obstacleStart(){
Rectangle ballBounds = ball.getBounds();
int tempX[] = {ballBounds.x,ballBounds.x+ballBounds.width,ballBounds.x,ballBounds.x+ballBounds.width};
int tempY[] = {ballBounds.y,ballBounds.y,ballBounds.y+ballBounds.height,ballBounds.y+ballBounds.height};
int count = 0;
// 获取所有的星星,进行循环检查
for (Component component : this.getComponents()) {
if(component instanceof Star){
Star obstacle = (Star)component;
Rectangle goalkeeperBounds = obstacle.getBounds();
int minX = goalkeeperBounds.x;
int maxX = goalkeeperBounds.x+goalkeeperBounds.width;
int minY = goalkeeperBounds.y;
int maxY = goalkeeperBounds.y+goalkeeperBounds.height;
miukoo:for (int i = 0; i < tempY.length; i++) {
// 如何球的4个点,在星星的区域内,则命中
if(tempX[i]>minX&&tempX[i]<maxX&&tempY[i]>minY&&tempY[i]<maxY){
System.out.println("================命中星星");
count++;
STAR_CLIP.play();
this.remove(obstacle);// 消除星星,自己删除即可
break miukoo;
}
}
}
}
return count;
}
5)守门员来回移动的逻辑实现
public void move() {
try {
Rectangle bounds = this.getBounds();
double width = bounds.getX();
if (isAdd) {// 向右
width += 30;
if (width < backgroundPanel.getWidth() * 4 / 5) {
backgroundPanel.repaint();
this.setBounds((int) width, bounds.y, bounds.width, bounds.height);
Thread.sleep(100);
} else {
isAdd = false;
}
} else {// 向左
width -= 30;
if (width > backgroundPanel.getWidth() * 1 / 5) {
backgroundPanel.repaint();
this.setBounds((int) width, bounds.y, bounds.width, bounds.height);
Thread.sleep(100);
} else {
isAdd = true;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
while (true&&!Thread.interrupted()){
move();
}
}
6)石头的逻辑实现
参考代码:
public class Shitou extends JLabel implements Obstacle {
BackgroundPanel backgroundPanel;
public Shitou(BackgroundPanel backgroundPanel){
this.backgroundPanel = backgroundPanel;
this.setBounds(backgroundPanel.getWidth()/2+50,100,316,100);//设置图片放置的位置
this.setPreferredSize(new Dimension(316,100));
this.setIcon(new ImageIcon(ResourcesUtil.getRootPath()+"\\ball\\st.png"));
}
@Override
public String name() {
return "石头";
}
@Override
public JComponent getComponent() {
return this;
}
public void start(){
}
public void stop(){
}
}
7)进球逻辑实现
ball.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
infoPanel.addCount();
new Thread(new Runnable() {
@Override
public void run() {
int star =0;
for (Point2D.Double point : line.getPoints()) {
ball.setBounds((int) point.x, (int) point.y,ball.getWidth(),ball.getHeight());
try {
star += obstacleStart();
Thread.sleep(100);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
// 轨迹点移动完成后,开始检查球是否进球
check(star);
}
}).start();
}
});
检查守门员与球的位置关系:
public void check(int star){
//停止所有的线程,以便获取守门员最后的位置,与球判重
stop();
Rectangle ballBounds = ball.getBounds();
// 检查是否被石头还有守门员
boolean isLan = obstacle();
int x = ballBounds.x;
int y = ballBounds.y;
boolean isOut = false;
if(!isLan) {
// 判断是否出界
if (x < getWidth() / 5 || x > getWidth() * 4 / 5 || y < 85) {
isOut = true;
FAIL_CLIP.play();
repaint();
JOptionPane.showMessageDialog(null, "球出界了...", "Tipe", JOptionPane.ERROR_MESSAGE);
}
}
if(!isLan&&!isOut){
// 没有被守住,也没有出界,则进球啦~~~
if(result!=null){
this.remove(result);
}
infoPanel.addScore(star);
System.out.println("===========赢了,开始显示祝贺彩带和播放音乐");
result = new Result();
result.setPreferredSize(new Dimension(230,187));
result.setBounds((getWidth()-230)/2,(getHeight()-187)/2,230,187);
this.add(result);
this.repaint();
WIN_CLIP.play();
// 进球后,休眠5秒,然后自动复位球
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
WIN_CLIP.stop();
this.remove(result);
}else{
FAIL_CLIP.stop();
}
line.setVisible(false);
ball.setBounds((getWidth()-64)/2,470,64,64);
// 重新开启线程,让守门员再次动起来
start();
}
检查球与障碍物的位置关系:
public boolean obstacle(){
Rectangle ballBounds = ball.getBounds();
int tempX[] = {ballBounds.x,ballBounds.x+ballBounds.width,ballBounds.x,ballBounds.x+ballBounds.width};
int tempY[] = {ballBounds.y,ballBounds.y,ballBounds.y+ballBounds.height,ballBounds.y+ballBounds.height};
for (Component component : this.getComponents()) {
if(component instanceof Obstacle){// 守门员和石头都抽象成障碍物,判断球是否与障碍物重合
Obstacle obstacle = (Obstacle)component;
Rectangle goalkeeperBounds = obstacle.getComponent().getBounds();
int minX = goalkeeperBounds.x;
int maxX = goalkeeperBounds.x+goalkeeperBounds.width;
int minY = goalkeeperBounds.y;
int maxY = goalkeeperBounds.y+goalkeeperBounds.height;
boolean isLan = false;
for (int i = 0; i < tempY.length; i++) {
if(tempX[i]>minX&&tempX[i]<maxX&&tempY[i]>minY&&tempY[i]<maxY){
isLan = true;
FAIL_CLIP.play();
repaint();
JOptionPane.showMessageDialog(null,"球被"+obstacle.name()+"守住了...","Tipe",JOptionPane.ERROR_MESSAGE);
return true;
}
}
}
}
return false;
}
8)记时逻辑实现
参考代码:
long time = 0;
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
time++;
} catch (InterruptedException e) {
e.printStackTrace();
}
repaint();// 刷新线程时间
// 显示时间时把time转成对应格式,g2d.drawString(String.format("%02d:%02d",time/60,time%60),345,47);
}
}
9)进球积分逻辑实现
相关实现逻辑思路如下:在球飞行过程中进来消除星星的数量;在进球时,把星星的数量当作分数累计。
在监听球移动的过程,判断其位置是否与星星重合:
ball.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
infoPanel.addCount();
new Thread(new Runnable() {
@Override
public void run() {
// 记录飞行过程中消除星星的数量
int star =0;
for (Point2D.Double point : line.getPoints()) {
ball.setBounds((int) point.x, (int) point.y,ball.getWidth(),ball.getHeight());
try {
// 计算星星是否被消除,如果是则累计
star += obstacleStart();
Thread.sleep(100);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
// 把消除星星作为分数,传递给计分区
check(star);
}
}).start();
CLICK_CLIP.play();
}
});
// 检查是否进球
public void check(int star){
........
if(!isLan&&!isOut){// 进球了
if(result!=null){
this.remove(result);
}
// 增加计分区的数字,有多少星星则记多少
infoPanel.addScore(star);
........
}