JAVA大作业编写思路

最近忙于各种学业问题,算法只能先搁置一波了……

本文出现的全部代码只是当时写完的版本,不是最终版本,最终版本(始终在更新)请移步github:MyPVZ

本来对大作业一筹莫展,在周日出去玩回来的地铁上被某人一语点醒:“可以做个植物大战僵尸呀!”

好,似乎植物大战僵尸也就是几个格子,判断一下碰撞之类的东西,应该比较好写,那就收集一下图片啥的资源开始做吧。(然而到现在还没写到判断碰撞的逻辑……真是太低估这东西的难度了)

2019.11.10

既然要写java,那得先会这门语言吧…还好两天前(周五)考过了java的期末,稍微搞明白了一点java与C系列语言直接面向过程编写代码的不同之处:首先得把一种东西视作对象,实现这个对象所要的需求,然后将这个对象实例化(new一个出来),进行所需要的一系列操作。

好像确实没什么的,那就开始写吧。

先找到了一份有img的源码,拿走img。

既然是个游戏,我寻思不然就用前几天刚分析的2048的套路,controller+view+model吧(不过搞到后来似乎都没有model)。

首先植物是个模型,先写个植物再说。

Plant.java

于是去学了一手JLabel和runnable实现动画效果,写了一下。

对于植物,实现了一些属性:名字name、价格price、血量hp、当前外观照片pic、产出冷却时间CD(产阳光、发射子弹等等)、图像是否可以修改canChange(update:当时觉得card类需要extend一下,所以留了这么一个接口,没想到以后card没用到,倒是其他地方起了作用)、控制器controller、位置row column、两次动画间隔时间sleepTime、对应卡片冷却时间cardCD。由于它们都是private的,需要很多public的接口供外面使用,所以添加了一堆东西,但这不是主要的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
//active model of Plants

package view;

import controller.*;
import javax.swing.*;
import java.awt.*;

public class Plant extends JLabel implements Runnable {
private static final long serialVersionUID = 1L;
private String name;
private ImageIcon img;

private int hp = 0;
private int price = 0;

private int pic = 0;
private int SumPic = 0;

private int CD; // 冷却时间
private int Cnow = 0; // 积累时间

private boolean canChange = false; // 图像是否为gif(区分card)
private Controller controller;
private int row, column;

private int sleepTime;

private int cardCD;// 卡片冷却时间

// 实现一些接口
public int getR(){
return row;
}

public int getC(){
return column;
}

public int getCardCD() {
return cardCD;
}

public int getCD() {
return CD;
}

public int getCnow() {
return Cnow;
}

public int getPic(){
return pic;
}

public void setCnow(int Cnow) {
this.Cnow=Cnow;
}

public void setPos(int row, int column) {
this.row = row;
this.column = column;
}

public void setController(Controller controller) {
this.controller = controller;
}

public Controller getController() {
return controller;
}

public String getName() {
return this.name;
}

public int getPrice() {
return this.price;
}

void attacked() {
this.hp--;
if (hp == 0) {
die();
}
}

public void die() {
// System.out.println(name + " is dead.");
controller.plantDeath(row, column);
this.setVisible(false);
}

public void attack() {
// to-do
}

同时实现了一些SunFlower、Plant初始化的小问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public Plant getPlant(String name) {
Plant newPlant = null;
switch (name) {
case "SunFlower":
newPlant = new Plant().SunFlower();
break;
//TO-DO
return newPlant;
}

public Plant() {
this.img = null;
this.name = null;
this.price = 0;
this.hp = 0;
//this.attack = 0;
this.SumPic = 0;
}

public Plant(String name, int price, int attack, int hp, int SumPic, boolean canChange) {
// System.out.println("Set " + name + ".");
// System.out.println("Price:" + price);
// System.out.println("Attack:" + attack);
// System.out.println("HP:" + hp);
this.canChange = canChange;
this.img = new ImageIcon("img\\" + this.name + "\\" + this.name + "_" + "0.png");
this.name = name;
this.price = price;
this.hp = hp;
//this.attack = attack;
this.SumPic = SumPic;
}

// various plants
// price attack hp sumpic canchange
public Plant SunFlower() {
Plant tempPlant = new Plant("SunFlower", 50, 0, 300, 17, true);
tempPlant.CD = 24000 / 90;
tempPlant.cardCD = 7500;
tempPlant.sleepTime = 90;
return tempPlant;
}
// TO-DO:PLANTS

主要的就是两部分:重写paintComponent和重写run。这也是贯穿整个项目外观的两个重要部分:线程运行run,外观通过paintComponent改变。

paintComponent主要实现了当前JLabel标签的image,在植物方面大概只需要画底部阴影shadow和植物主体img就可以了。

run是Runnable接口调用的关键,是线程运行的主体。当植物存活的时候,不停积累产出冷却所需时间并重画(repaint),期间需要让线程sleep一段时间保证计算不过于频繁。

关于线程,参考资料:Java Thread线程使用、线程安全(一) - 简书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
    void picChange() {
pic = (pic + 1) % SumPic;
img = new ImageIcon("img\\" + this.name + "\\" + this.name + "_" + pic + ".png");
}

void accumulate() {// accumulate to CD?
if (this.canChange == false)
return;
this.picChange();
this.Cnow = this.Cnow + 1;
}

@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
// 植物底部阴影
ImageIcon shadow = new ImageIcon("img\\shadow.png");
g2.drawImage(shadow.getImage(), -11, 45, shadow.getIconWidth(), shadow.getIconHeight(), null);
g.drawImage(img.getImage(), 0, 0, img.getIconWidth(), img.getIconHeight(), null);
}
}

@Override
public void run() {
while (hp >= 0) {
try {
Thread.sleep(sleepTime);
this.accumulate();
if(this.getCnow()>=this.getCD()){
this.setCnow(0);
//TO-DO:SunProduce
}
this.repaint();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

大概plant部分就到此为止。

Controller.java

接下来就是最主要的控制器了。

控制器是各个部件的中枢,所有的对象通过这个控制器对其他对象产生影响。

所以除了要实现植物的矩阵之外,也要预留一下卡片、小车、僵尸、太阳等的位置(在此没有预留小车位置),同时在种植过程中,图片应当跟随鼠标进行显示,因此还需要一个鼠标事件监听器和JPanel。

同时,控制器需要向gameboardview传输信号,因此将整个gameboard作为一个JLayeredPane处理。

关于JFrame的层次结构,参考资料:JFrame 的层次结构 - CSDN

关于鼠标事件监听器,参考资料:MouseListener与MouseAdapter的区别 - CSDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
package controller;

import view.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.HashMap;
import java.util.Map;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;

public class Controller {
//appearance
private JFrame frame;
private static int cardHeight = 90, cardWidth = 45;
private static int tileHeight = 100, tileWidth = 80;

//plants
private Plant[][] plants = new Plant[5][9];
public Plant nowPlant = null;

//sun
private JLabel sunCount = new JLabel();

//mouse
private Point mouseP;
private JLayeredPane layeredPane;
private TopPanel topPanel = new TopPanel();
private ImageIcon preImg;
private ImageIcon blurImg;

//state
public boolean isRunning=false;

public void setLayeredPane(JLayeredPane gameboardView) {
this.layeredPane = gameboardView;
}

public JLayeredPane getLayeredPane(){
return layeredPane;
}

public void setFrame(JFrame frame) {
this.frame = frame;
}

public JFrame getFrame() {
return this.frame;
}

public Point getP() {
return topPanel.getMousePosition();
}

public void setP(Point p) {
this.mouseP = p;
}

public JPanel getTopPanel() {
return topPanel;
}

public void setPreImg(ImageIcon preImg) {
this.preImg=preImg;
}

public void setBlurImg(ImageIcon blurImg) {
this.blurImg=blurImg;
}

//鼠标移动效果,选中的植物
class TopPanel extends JPanel {
private static final long serialVersionUID = 1L;
Graphics2D g2;

public void paintComponent(Graphics g) {
super.paintComponent(g);
g2 = (Graphics2D) g;
Point mouseP = getP();
if (mouseP != null) {
int c = (mouseP.x - 40) / 80;
int r = (mouseP.y - 90) / 100;
if (isOnGrass(r, c) && plants[r][c] == null) {
g2.drawImage(blurImg.getImage(), 40 + c * 80, 90 + r * 100, this);
}
g2.drawImage(preImg.getImage(), mouseP.x - 35, mouseP.y - 40,
preImg.getIconWidth(),
preImg.getIconWidth(), this);
}
}

public void update(Graphics g) {
Image offScreenImage = this.createImage(800, 600);
Graphics gImage = offScreenImage.getGraphics();
paint(gImage);
if (mouseP != null) {
g.drawImage(offScreenImage, mouseP.x - 35, mouseP.y - 40, null);
}
}
}

public void plantDeath(int row, int column) {
if (plants[row][column] != null) {
System.out.println("Error occurred:deleted a non-exist plant from (" + row + "," + column + ")");
}
//System.out.println(plants[row][column].getName() + " is dead at(" + row + "," + column + ")");
plants[row][column] = null;
}

private boolean isOnGrass(int r, int c) {// (r,c)->grass
return r >= 0 && r < numRow && c >= 0 && c < numCol;
}

//鼠标选中的植物卡牌下标
private int selectedIndex = -1;

public void setSelectedIndex(int x){
this.selectedIndex = x;
}

public int getSelectedIndex(){
return selectedIndex;
}

class myMouseListener extends MouseAdapter implements MouseMotionListener {
private TopPanel topPanel;

myMouseListener(TopPanel topPanel) {
this.topPanel = topPanel;
}

public void mouseClicked(MouseEvent e) {
Point p = getP();
if(p==null)return;
//TO-DO 种植
}
public void mouseMoved(MouseEvent e){
setP(e.getPoint());
topPanel.repaint();
}
public void mousePressed(MouseEvent e) {}
public void mouseReleased(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseDragged(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
}

public void plant(int r,int c,Plant plant){
plant.setController(this);
plant.setVisible(true);
plant.setBounds(45 + c * 80, 90 + r * 100, 300, 300);
plant.setPos(r, c);
layeredPane.add(plant, new Integer(50));
plants[r][c]=plant;
new Thread(plant).start();
}

public boolean findZombie(int row,int column){
return false;//TO-DO
}

public Controller(){
setCardMap();
cardNum=0;
this.setSunCount(150);
this.topPanel.addMouseMotionListener(new myMouseListener(this.topPanel));
this.topPanel.addMouseListener(new myMouseListener(this.topPanel));
this.topPanel.setVisible(false);
isRunning=true;
}
}

2019.11.11

GameboardView.java

我就是调参大师.jpg

实现了上来左右摇摆对准的功能以及倒计时,并分配了cardboard的进入动画和鼠标图片topPanel。

移动方面有待改进……效果还可以,留到以后再改。

这部分对准参数之后没啥好说的。每个动画效果都是在不停的对坐标进行修改、对Panel进行repaint做出来的。

这里panel、Panel分别表示了背景图片和Panel,cardboard和Cardboard同理。这里命名有问题,以后再做修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
package view;

import controller.*;

import java.awt.Graphics;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javax.swing.*;

public class GameboardView extends JLayeredPane {

private static final long serialVersionUID = 1L;
private JFrame GameFrame;
private ImageIcon panel, cardboard;
private JPanel Panel;
private JPanel Cardboard;

Controller controller;

private int x = -10;
private boolean flag = false;
private int direction = 1;
private JLabel SunLabel;

class PaintThread implements Runnable {
JFrame frame;

PaintThread(LaunchFrame launchFrame) {
this.frame = launchFrame;
}

@Override
public void run() {
try {
Thread.sleep(1600);
} catch (InterruptedException e) {
e.printStackTrace();
}

//左右移动对准
for (int i = 0; i < 895; i++) {
// System.out.println("now x:" + x + ",i:" + i);
if (x <= -560 && !flag) {
flag = true;
direction = -1;
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int j=i-551;
int delaytime=
i<1?10:
i<2?9:
i<3?8:
i<4?7:
i<5?6:
i<6?5:
i<7?4:
i<8?3:
i<9?2:
i<534?1:
i<537?2:
i<540?3:
i<544?4:
i<547?5:
i<549?6:
i<550?8:14;
if(j>0){
delaytime=
j<1?12:
j<2?11:
j<3?10:
j<4?9:
j<5?8:
j<6?7:
j<7?6:
j<8?5:
j<9?4:
j<10?3:
j<12?2:
j<314?1:
j<319?2:
j<324?3:
j<327?4:
j<330?5:
j<333?6:
j<334?8:14;
}
try {
Thread.sleep(delaytime);
} catch (InterruptedException e) {
e.printStackTrace();
}
x-=direction;
Panel.repaint();
}

//cardboard淡入
for (int y = -40 ; y <= 5; y++) {
Cardboard.setBounds(20,y,cardboard.getIconWidth(), cardboard.getIconHeight());
try {
Thread.sleep(10);
}catch (InterruptedException e) {
e.printStackTrace();
}
Cardboard.repaint();
}

//倒计时
JLabel label;
for (int i=1 ; i<=3 ; i++){
try {
label = new JLabel(new ImageIcon("img\\PrepareGrowPlants"+i+".png"));
GameboardView.this.add(label,1);
label.setBounds(250,200,300,200);
Thread.sleep(700);
label.setVisible(false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}


GameboardView(LaunchFrame launchframe){
this.controller=new Controller();
this.GameFrame=launchframe;
this.GameFrame.setContentPane(GameboardView.this);
this.setVisible(true);

//bg
panel=new ImageIcon("img\\background1.jpg");
Panel=new JPanel(){
private static final long serialVersionUID = 1L;
public void paintComponent(Graphics g){
super.paintComponent(g);
g.drawImage(panel.getImage(),x,0,
this.getWidth(),this.getHeight(),
this);
}
};
Panel.setVisible(true);
Panel.setBounds(0,0,panel.getIconWidth(),panel.getIconHeight());
GameboardView.this.add(Panel,-1);

//cardboard
cardboard=new ImageIcon("img\\SeedBank.png");
Cardboard=new JPanel(){
private static final long serialVersionUID = 1L;
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(cardboard.getImage(),0,0,
cardboard.getIconWidth(),
cardboard.getIconHeight(),this);
}
};
Cardboard.setVisible(true);
Cardboard.setLayout(null);
GameboardView.this.add(Cardboard,0);

Executor exec = Executors.newSingleThreadExecutor();
Thread Animation=new Thread(new PaintThread(launchframe));
exec.execute(Animation);

JPanel topPanel = controller.getTopPanel();
topPanel.setVisible(false);
topPanel.setBounds(0,0, panel.getIconWidth(), panel.getIconHeight());
GameboardView.this.add(topPanel, new Integer(114514));

}

}

LaunchFrame.java

实现了一个开始界面(留了很多余地给之后分配,这里不是游戏主体先不去做扩展)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package view;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;


public class LaunchFrame extends JFrame{

private static final long serialVersionUID = 1L;
private JLayeredPane layeredPane = new JLayeredPane();

public LaunchFrame(){
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("PVZ");
this.setSize(810,625);
this.setResizable(false);
this.setLocationRelativeTo(null);
this.setVisible(true);
this.setContentPane(layeredPane);
this.setIconImage(new ImageIcon("img\\Icon.png").getImage());

ImageIcon background=new ImageIcon("img\\Surface.png");
JPanel panel=new JPanel(){
private static final long serialVersionUID = 1L;
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(background.getImage(),0,0,
this.getWidth(),this.getHeight(),this);
}
};
panel.setBounds(0,0,background.getIconWidth(),background.getIconHeight());
layeredPane.add(panel,10);

ImageIcon startImg=new ImageIcon("img\\SelectorScreen_Adventure_highlight.png");
ImageIcon startImg2=new ImageIcon("img\\SelectorScreen_StartAdventure_Highlight.png");
JLabel startLabel=new JLabel(startImg);
startLabel.addMouseListener(new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
startLabel.setIcon(startImg2);
}
public void mouseExited(MouseEvent e) {
startLabel.setIcon(startImg);
}
public void mouseClicked(MouseEvent e){
layeredPane.setVisible(false);
new GameboardView(LaunchFrame.this);
}
});
startLabel.setBounds(410,70,startImg.getIconWidth(),startImg2.getIconHeight());
layeredPane.add(startLabel, 0);
}

}

Main.java

1
2
3
4
5
6
7
import view.*;

public class Main{
public static void main(String[] args) {
new LaunchFrame();
}
}

写完这些之后终于可以运行了……感觉还不错(疯狂debug)

2019.11.12

把卡片和太阳给做了(

Card.java

卡片类,实现选中/非选中状态的图片切换,在Controller.java、Plant.java中也有对应修改,在此不放出。

TO-DO:实现进度条式改变冷却图片,不是只有一黑一白(注释掉了一行失败的尝试)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package view;

import controller.*;

import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Map;

import javax.swing.ImageIcon;
import javax.swing.JLabel;

public class Card extends JLabel implements MouseListener, Runnable {

private static final long serialVersionUID = 1L;
private boolean inCooling;
private int cd;
private int totTime;

private String cardName;
private int price;
private int cardWidth;
private int cardHeight;

private ImageIcon card;
private ImageIcon cardLight;
private ImageIcon cardDark;

private ImageIcon preImg;
private ImageIcon blurImg;

private Map<String, Plant> plantMap;
private Controller controller;
private int y;

private Rectangle rectangle;//卡片位置

private int index;

public int getIndex(){
return index;
}

public void setIndex(int index){
this.index = index;
}

public void setRectangle(int x, int y, int w, int h) {
this.rectangle = new Rectangle(x, y, w, h);
}

public Rectangle getRectangle() {
return rectangle;
}

public Card(String name, Controller controller) {
this.controller = controller;
this.plantMap = controller.getPlantMap();
this.cardName = name;
this.cardLight = new ImageIcon("Img\\Cards\\" + cardName + "0.png");
this.cardDark = new ImageIcon("Img\\Cards\\" + cardName + "1.png");
this.preImg = new ImageIcon("img\\" + cardName + "\\" + cardName + "_0.png ");
this.blurImg = new ImageIcon("img\\Blurs\\" + cardName + ".png ");
this.card = cardDark;
this.inCooling = false;
this.addMouseListener(this);
this.cardHeight = cardLight.getIconHeight();
this.cardWidth = cardLight.getIconWidth();
this.price = plantMap.get(cardName).getPrice();
this.cd = plantMap.get(cardName).getCardCD();
this.y = card.getIconHeight();
}

public void check(int SunCount) {//能否选择这个卡,能:light,否:dark
if (SunCount >= plantMap.get(this.cardName).getPrice() && !inCooling) {
card = cardLight;
this.repaint();
} else {
card = cardDark;
this.repaint();
}
}

public void setInCooling(boolean b) {
this.inCooling = b;
}

public boolean getInCooling() {
return inCooling;
}

public int getPrice() {
return price;
}

public String getCardName() {
return cardName;
}

public int getCardWidth() {
return cardWidth;
}

public int getCardHeight() {
return cardHeight;
}

@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
//int h=card.getIconHeight();
g.drawImage(card.getImage(), 0, 0, card.getIconWidth(), card.getIconHeight(), this);
//g.drawImage(card.getImage(), 0, h-h*totTime/cd, card.getIconWidth(), h*totTime/cd, this);
g.drawImage(cardDark.getImage(), 0, y, card.getIconWidth(), card.getIconHeight(), this);
}

public void selected() {//卡牌被选中
if (!inCooling && controller.getIntSunCount() >= this.getPrice()) {
controller.getTopPanel().setVisible(true);
controller.setPreImg(this.preImg);
controller.setBlurImg(this.blurImg);
controller.setCard(this);
controller.setSelectedIndex(index);
controller.nowPlant = new Plant().getPlant(this.getCardName());
//System.out.println("Selected :" + controller.nowPlant.getName());
inCooling = true;
check(0);
}
}

@Override
public void mouseClicked(MouseEvent e) {
selected();
}
@Override public void mouseEntered(MouseEvent e) {}
@Override public void mouseExited(MouseEvent e) {}
@Override public void mousePressed(MouseEvent e) {}
@Override public void mouseReleased(MouseEvent e) {}

@Override
public void run() {
while (controller.isRunning) {
while (controller.isRunning && !inCooling) {
check(controller.getIntSunCount());
try {
Thread.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
check(controller.getIntSunCount());
while(controller.isRunning){
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
totTime+=3;
if(totTime>=cd){
break;
}
check(controller.getIntSunCount());
}
totTime=0;
inCooling=false;
}
}

}

Sun.java

阳光有两种方式生成,向日葵掉落或者随机从天上生成。分为三个阶段,掉落、停留、消失。掉落阶段一个是抛物线,一个是直线,分别模拟一下就可以了,这里抛物线用若干条线段进行模拟;停留阶段进行旋转;消失阶段进行边收集边消失或者直接淡出。

这里太阳逐渐消失用到了一个神秘函数:g2.setComposite(AlphaComposite.SrcOver.derive((float) alpha / 100));

其中alpha表示不透明度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package view;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import controller.*;

public class Sun extends JLabel implements Runnable {

private static final long serialVersionUID = 1L;

private Image image, offScreenImage;

private int x, y;
private boolean isCollected;
private int nowPic;
private static int totPic = 21;
private Controller controller;

private boolean drop;// 掉落还是随机生成

// 随机生成:
private int ymax;// 下

// 向日葵掉落:
private int ymin;
private int startX,startY, endX,endY;

public Sun(Controller controller) {
this.x = (int) (Math.random() * 660) + 30;
this.ymin = this.y = 40;
this.ymax = (int) (Math.random() * 400) + 130;
this.controller = controller;
this.drop = false;
setVisible(true);
controller.getLayeredPane().add(this, new Integer(500));
}

public Sun(Controller controller, int r, int c) {
this(controller);
this.drop = true;
this.ymin = r * 100 + 55;
startX=c * 80 + 34 + (int) (Math.random() * 2);
startY= r * 100 + 80;
endX=c * 80 + 30 + (int) (Math.random() * 10);
endY=r * 100 + 100;
this.x = startX;
this.y = startY;
}

public void picChange() {
nowPic = (nowPic + 1) % totPic;
}

public void shrink() {
for (int i = 1; i <= 50; i++) {
if (i % 8 == 0) {
picChange();
}
alpha -= 2;
try {
Thread.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
repaint();
}
setVisible(false);
}

@Override
public void run() {
this.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
if (!isCollected) {
isCollected = true;
}
}
});

//dropping
if (drop) {
// 掉落,用线段近似二次函数
int vx=1,vy=2;
boolean flag1=false,flag2=false,flag3=false,flag4=false;
int dx=(endX>=startX)?1:-1,dy=-1;
for(int i = 1; (this.x<=endX&&dx==1)||(this.x>=endX&&dx==-1) || this.y<=endY; i++){
if(i%5==0){
this.picChange();
}
this.setBounds(x,y,78,78);
try{
Thread.sleep(10);
}catch(InterruptedException e1){
e1.printStackTrace();
}
x+=dx*vx;y+=dy*vy;
if(!flag1){
if(y<=(ymin+startY)/2){
flag1=true;
vy=1;
};
}else if(!flag2){
if(y<=ymin){
vy=-1;
flag2=true;
}
}else if(!flag3){
if((x>=(endX*3+startX)/4||y>=(ymin+endY)/2)){
vy=-2;
flag3=true;
}
}else if(!flag4){
if((x>=(endX*7+startX)/8||y>=(startY+endY)/2)){
vy=-3;
flag4=true;
}
}
this.repaint();
}
} else {
// 随机生成,直线
for (int i = 1; this.y < ymax && !isCollected; i++) {
if (i % 50 == 0) {
this.picChange();
}
this.setBounds(x, y, 78, 78);
try {
Thread.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
if (i % 10 == 0)
this.y++;
this.repaint();
}
}

// onGround
for (int i = 1; i <= 5000 && !isCollected; i++) {
if (i % 50 == 0) {
this.picChange();
}
try {
Thread.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
this.repaint();
}

// collected
Point tempPoint=new Point(x,y);
if (isCollected) {
controller.addSunCount(25);
controller.checkCards();
int dir=x<33?-1:1;
for (int i = 1; (dir==1&&x > 33||x<=33&&dir==-1) && y > 33; i++) {
if (i % 10 == 0) {
this.picChange();
}
double ty=(x-33)*(tempPoint.getY()-33)/(tempPoint.getX()-33)+33;
if((ty<=y&&dir==1)||(ty>=y&&dir==-1)){
y--;
}else{
x--;
if((int)((100*x-3200)/(tempPoint.getX()-33))!=(int)((100*x-3300)/(tempPoint.getX()-33))){
alpha-=(Math.random()>=0.45)?1:0;
//System.out.println("alpha reduced from point ("+x+","+y+")");
}
}

this.setBounds(x, y, 78, 78);
this.repaint();
try {
Thread.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}else{
//disappear
this.shrink();
}
setVisible(false);
Thread.currentThread().interrupt();
}

private double alpha = 100;

@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
ImageIcon img = new ImageIcon("img\\Sun\\"+nowPic+".png");
image = img.getImage();
Graphics2D g2 = (Graphics2D) g;
g2.setComposite(AlphaComposite.SrcOver.derive((float) alpha / 100));
g2.drawImage(image, 0, 0, img.getIconWidth(), img.getIconWidth(), this);
}
public void update(Graphics g) {
if(offScreenImage == null) offScreenImage = this.createImage(800, 600);
Graphics gImage = offScreenImage.getGraphics();
paint(gImage);
g.drawImage(offScreenImage, 0, 0, null);
}

}

SunProducer.java

负责从天上随机掉落阳光。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package view;

import controller.*;

public class SunProducer implements Runnable{
private Controller controller;

SunProducer(Controller controller){
this.controller = controller;
}

@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (controller.isRunning){
try {
Thread.sleep((int) (Math.random()*1000)+7000);
//7~8s随机生成一个
new Thread(new Sun(controller)).start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Thread.currentThread().interrupt();
}
}

到此为止,基本的向日葵有关的操作已经结束了。然后又向Plant、controller、card里面加了点关于豌豆射手的东西,接下来就该添加僵尸和子弹了。

这里用了个map存名字和植物类型的关系,大概可以参考一下java笔记–Map的用法 - CSDN

2019.11.15

由于前几天考计组,又在补作业,被迫停了一段时间。这段时间里,梳理了一下大致需要做的方向:

普通僵尸位置、碰撞、啃食、死亡逻辑;普通僵尸生成;子弹发射、碰撞逻辑;done

新植物及其逻辑;新僵尸及其逻辑;

对向日葵将要产出太阳时的动画进行优化;

设置关卡;

在初始界面设置其他操作;

在游戏中设置暂停、退出、重新开始、返回主菜单等功能。

不知道这些东西能实现到哪里?

Zombie.java

本以为僵尸不是那么难写,结果又要掉胳膊又要掉头还要有啃食动画,搞不好被樱桃炸弹炸糊了还得有个特殊的动画,这还不算多,搜了一下还有临界血量,低于这个血量虽然失去攻击力但还能吸收伤害并不断掉血……

好麻烦……

(还没写完,正在写)

(感觉真的好难写)

update: 写完了。

实现了走动、掉头(走动时及啃食时)、死亡的动画,实现了临界血量。

没掉胳膊,,,不想实现了(没图源),如果需要实现还要实现一个状态自动机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package view;

import javax.swing.*;
import java.awt.*;
import controller.*;

public class Zombie extends JLabel implements Runnable {
private static final long serialVersionUID = 1L;

private static int MOVE = 1;
private static int ATTACK = 2;
private static int LOSTHEAD = 3;
private static int LOSTHEADATTACK = 4;
private static int DIE = 5;
private static int BOOM = 6;

private static int BOOMINJURY = 1000;

private String name;
// hp2:临界值
// attack: /s
private int hp, hp2, state = 1;
// 走一格时间/格长=走1的时间
// private int v;

private int x, y;
private int row;

private ImageIcon img;
private int[] sumPic = new int[3];
private int nowSumPic;
private int nowPic = 0;
private int type;

private Controller controller;

public int getXPos() {
return x;
}

public Zombie() {

}

public Zombie(Controller controller, String name, int hp, int hp2, int row, int v) {
setVisible(true);
this.nowPic = 0;
this.controller = controller;
this.name = name;
this.hp = hp + hp2;
this.hp2 = hp2;
this.y = row * 100 + 28;
this.x = 800;
this.row = row;
this.sumPic[0] = 22;
this.sumPic[1] = 31;
this.sumPic[2] = 18;
this.setState(MOVE);
controller.getLayeredPane().add(this, new Integer(400));
}

@Override
public void paintComponent(Graphics g) {
ImageIcon Img;
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
img = new ImageIcon("img\\shadow.png");
g.drawImage(img.getImage(), 70, 115, img.getIconWidth(), img.getIconHeight(), this);
if (state == MOVE) {
Img = new ImageIcon("img\\Zombie" + type + "\\Frame" + nowPic + ".png");
g2.drawImage(Img.getImage(), 0, 0, Img.getIconWidth(), Img.getIconHeight(), this);
} else if (state == ATTACK) {
Img = new ImageIcon("img\\ZombieAttack\\Frame" + nowPic + ".png");
g2.drawImage(Img.getImage(), 0, 0, Img.getIconWidth(), Img.getIconHeight(), this);
} else if (state == LOSTHEAD) {
Img = new ImageIcon("img\\ZombieLostHead\\Frame" + nowPic + ".png");
g2.drawImage(Img.getImage(), 0, 0, Img.getIconWidth(), Img.getIconHeight(), this);
// 头动画只持续前10帧
if (nowPic < 10) {
Img = new ImageIcon("img\\ZombieHead\\Frame" + nowPic + ".png");
g2.drawImage(Img.getImage(), 60, 0, Img.getIconWidth(), Img.getIconHeight(), this);
}
} else if (state == LOSTHEADATTACK) {
Img = new ImageIcon("img\\ZombieLostHeadAttack\\Frame" + nowPic + ".png");
g2.drawImage(Img.getImage(), 0, 0, Img.getIconWidth(), Img.getIconHeight(), this);
} else if (state == DIE) {
Img = new ImageIcon("img\\ZombieDie\\Frame" + nowPic + ".png");
g2.drawImage(Img.getImage(), 0, 0, Img.getIconWidth(), Img.getIconHeight(), this);
} else if (state == BOOM) {
Img = new ImageIcon("img\\ZombieBoom\\Frame" + nowPic + ".png");
g2.drawImage(Img.getImage(), 0, 0, Img.getIconWidth(), Img.getIconHeight(), this);
}
}

public Zombie normalZombie(Controller controller, int row) {
Zombie tempZombie = new Zombie(controller, "NormalZombie", 200, 70, row, 4700 / 80);
tempZombie.type = (int) Math.random() * 3;
return tempZombie;
}

public void setState(int state) {
this.state = state;
nowPic = 0;
if (state == MOVE) {
this.nowSumPic = sumPic[type];
} else if (state == ATTACK) {
this.nowSumPic = 21;
} else if (state == LOSTHEAD) {
this.nowSumPic = 18;
} else if (state == LOSTHEADATTACK) {
this.nowSumPic = 11;
} else if (state == DIE) {
this.nowSumPic = 14;
} else if (state == BOOM) {
this.nowSumPic = 20;
}
}

public void updateState() {
if (hp <= 0) {
if (this.state != DIE && this.state != BOOM)
setState(DIE);
} else if (hp <= hp2) {
if (this.state == ATTACK)
setState(LOSTHEADATTACK);
else if(this.state != LOSTHEAD)
setState(LOSTHEAD);
}
}

public Plant getPlant() {
assert (findPlant());
return controller.getPlants()[row][(x + 60) / 80];
}

public boolean findPlant() {
if ((x + 60) / 80 >= 9)
return false;
return controller.getPlants()[row][(x + 60) / 80] != null;
}

public void reduceHP(int x) {
this.hp -= x;
updateState();
//System.out.println("reduced to "+hp+",state="+state);
}

public void boom() {
if (this.hp > BOOMINJURY) {
reduceHP(BOOMINJURY);
} else {
this.hp = 0;
setState(BOOM);
}
}

@Override
public void run() {
setState(MOVE);
while (hp > hp2) {
// MOVE
while (this.state == MOVE) {
if (findPlant()) {
setState(ATTACK);
break;
}
// sleep 60ms, x--
for(int j = 0; j < 2; j++) {
for (int i = 0; i < 10 && this.state == MOVE; i++) {
try {
Thread.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.x--;
this.setBounds(x, y, 400, 300);
this.repaint();
}
//120ms
nowPic = (nowPic + 1) % nowSumPic;
}

// ATTACK
while (this.state == ATTACK) {
if (this.name == "NormalZombie") {
// 普通僵尸的攻击方式:1s 200伤害,5ms 1伤害
// sleep 120ms change
for (int i = 0; i < 24 && this.state == ATTACK && findPlant(); i++) {
getPlant().attacked(1);
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
nowPic = (nowPic + 1) % nowSumPic;
this.repaint();
if (!findPlant())
setState(MOVE);// 不掉胳膊
}
}

}
while (hp > 0) {
// 临界状态
while (this.state == LOSTHEAD || this.state == LOSTHEADATTACK) {
//sleep 60ms change
for(int j = 0; j < 2; j++) {
for (int i = 0; i < 10 && hp > 0; i++) {
try {
Thread.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.x--;
this.setBounds(x, y, 400, 300);
this.repaint();
}
//120ms
nowPic = (nowPic + 1) % nowSumPic;
reduceHP(7);
}

controller.deleteZombie(this, row);

// 死亡
while (true) {
for (int i = 0; i < nowSumPic; i++) {
//sleep 120ms change
try {
Thread.sleep(120);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.repaint();
nowPic = (nowPic + 1) % nowSumPic;
}
break;
}
}
while (this.state == BOOM) {
// 爆炸
while (true) {
for (int i = 0; i < nowSumPic; i++) {
//sleep 50ms change
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.repaint();
nowPic = (nowPic + 1) % nowSumPic;
}
break;
}
}
setVisible(false);
Thread.currentThread().interrupt();
}
}

ZombieProducer.java

实现随机生成一堆僵尸的功能,以后还需要改成一波波僵尸的样子来配合关卡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package view;

import controller.*;

public class ZombieProducer implements Runnable{
private Controller controller;

ZombieProducer(Controller controller){
this.controller = controller;
}

@Override
public void run() {
try {
Thread.sleep(20000);
//开局20s无生成
} catch (InterruptedException e) {
e.printStackTrace();
}
while (controller.isRunning){
try {
Thread.sleep((int) (Math.random()*1000)+1000);
//7~8s随机生成一个
int row=(int)(Math.random()*5);
Zombie tempZombie=new Zombie().normalZombie(controller,row);
controller.addZombie(tempZombie,row);
new Thread(tempZombie).start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Thread.currentThread().interrupt();
}
}

Bullet.java

配合plant、controller对僵尸进行打击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package view;

import controller.*;
import javax.swing.*;
import java.awt.*;

public class Bullet extends JLabel implements Runnable {

private static final long serialVersionUID = 1L;
private Controller controller;
private ImageIcon img;
private int x, y, row;

@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.drawImage(img.getImage(), 0, 0, img.getIconWidth(), img.getIconHeight(), this);
}

public Bullet() {
}

public Bullet(Controller controller, int row, int column) {
setVisible(true);
img = new ImageIcon("img\\Bullets\\PeaShooter.png");
this.controller = controller;
this.row = row;
this.x = 40 + column * 80 + 40;
this.y = 90 + row * 100;
this.setBounds(x, y, img.getIconWidth(), img.getIconHeight());
controller.getLayeredPane().add(this, new Integer(500));
}

public Bullet Pea(Controller controller, int row, int column) {
Bullet tempBullet = new Bullet(controller, row, column);
return tempBullet;
}

public void bulletAttack(Zombie zombie) {
zombie.reduceHP(20);
}

@Override
public void run() {
while (true) {
Zombie tempZombie = controller.getAttackedZombie(row, x - 40);
if (tempZombie != null) {
this.bulletAttack(tempZombie);
break;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
x += 5;
this.setBounds(x, y, img.getIconWidth(), img.getIconHeight());
this.repaint();
if (x > 810)
break;
}
// boom
img = new ImageIcon("img\\Bullets\\PeaShooterHit.png");
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
setVisible(false);
Thread.currentThread().interrupt();
}

}

其他的controller、plant等部分也有很多调整,其中controller中实现了对于僵尸群的列表表示,使用了ArrayList类型的List分别表示五行中的僵尸,详情还是见github上的源码吧。

2019.11.16

添加了双发射手,修复了各种逻辑的小bug

并添加了etz阳光和xsy僵尸的逻辑(照片不上传)