关于我做了一款原神
新的一年想掌握一项新的技能,于是花两周看完了java的基本语法,找到了贪吃蛇作为自己的第一个项目,由于是新手,代码中类的方法十分杂乱,但是在编写过程中解决了几乎所有的疑点,能够说出每一条代码的作用,以此篇文章作为知识点的总结。如果你在独自编写贪吃蛇的时候遇到了问题,不妨可以看看这篇文章,关于标题为什么是派蒙贪吃蛇,当然是因为我往里面添加了原的元素啦。此外,里面还添加了音乐和读取历史最高纪录的功能。
一.框架的搭建
这里不用多说,用到的自然是JFrame,所有用户图形用到的都是他,很多视频都有介绍,我便不再赘述
this.setSize(800, 600);//框架大小
this.setLocationRelativeTo(null);//居中
this.setTitle("原神");//框架标题
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//关闭模式
二.画板的插入
我们想要往界面中绘制图形,用到的是重写后的paint(Graphics g)方法,因为他里面有个画笔参数Graphics g,而我们想要画任何图像都需要画笔,所以一切关于图像的绘制都需要写到paint里面去,paint()方法会在对象初始化的时候自动执行一次,不需要调用,而且也调用不了,因为形参画笔g不允许我们自己new一个,也就是说往别的方法里塞画笔是做不到的
public void paint(Graphics g) {
g.setColor(Color.red);//设置画笔的颜色,此为红
g.fillRect(0, 0, 600, 600);//造一块跟框架一样大的矩形当做背景
g.setColor(Color.black);//更改画笔的颜色
for (int i = 0; i < 20; i++) {
g.drawLine(0, 30*(i+1), 600, 30*(i+1));
g.drawLine(30*(i+1), 0, 30*(i+1), 600);
}//drawline方法为划线,四个参数为起点和重点的横纵坐标,这样使蛇的轨迹更为清晰
前面已经说到所有的绘画行为需要在paint中写出,那么蛇的绘制和个性化面板也不例外
蛇的位置我是用两个整形数组来储存的,但总感觉这样很浪费内存,因为每次移动都需要后一段身体取代前一段的位置,但实际上蛇的移动只有头往前一格,尾巴再占据头原来的位置两步,但数组因为索引不可变实现不了这样的逻辑,爱钻研的小伙伴可以自己去找找有没有这样的类数组
//创造派蒙蛇
//派蒙身体
for (int i = 1; i <length ; i++) {
new ImageIcon("图片/纠缠之缘.jpg").paintIcon(this, g, x[i], y[i]);
身体长度我用length来表示,初始为3,以后吃到原石了就加1
} //派蒙头
switch (head){case "d": new ImageIcon("图片/派蒙右.jpg").paintIcon(this, g, x[0], y[0]);break;
case "a": new ImageIcon("图片/派蒙左.jpg").paintIcon(this, g, x[0], y[0]);break;
case "w": new ImageIcon("图片/派蒙上.jpg").paintIcon(this, g, x[0], y[0]);break;
case "s": new ImageIcon("图片/派蒙下.jpg").paintIcon(this, g, x[0], y[0]);break;}//由于蛇在转向的时候头会转,所以得有四张头的照片,参数head代表头的方向,我用常玩的电脑awsd来表示,head不知何用的请继续看后面,涉及到监听
if(exist==true){
new ImageIcon("图片/原石.png").paintIcon(this, g, 30*yx, 30*yy); }
yx,yy是我随机出的两个整数,*30用来表示其刷新的位置
这里可以看到我使用imageicon(路径).painticon(this,g,横纵坐标)插入了图片,这里的this指的是框架,即画到框架当中,g为画笔,关于图像的插入似乎还有个image,但我还不甚明了。总之,从蛇的头到length每张贴图已经挂上去了
三.关于监听
首先是键盘监听,这个很好理解,在创建类的时候implements一个KeyListener,然后会重写关于按下,松开,仅字符按下松开 某键的三个方法,这样我们根据Java里面设置好的各键对应的数字或常量,就能让键盘调用一些方法,比如我的派蒙蛇按下空格后会暂停,是通过布尔值isstart实现的,因为我把蛇的移动通通写到if(isstart为false)的大括号中,我把按下空格绑定{sstart=!isstart}这样就实现了空格键暂停/继续
@Override//按键监听
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if(keyCode==KeyEvent.VK_SPACE){//按下空格
if(isstart==true&&fail==true)
{this.unit();}isstart=!isstart;//这一句为撞到身体死亡后再按下空格则重来,unit为蛇的初始化
System.out.println(isstart); repaint();}
if (isstart==false) {
if(head!="d"){ if(keyCode==37){head="a";}}
if(head!="s"){if(keyCode==38){head="w";}}
if(head!="a"){if(keyCode==39){head="d";}}
if(head!="w"){ if(keyCode==40){head="s";}}
}//37,38,39,40代表的是键盘上的上下左右键,即根据这里使头转向
}
记住要写this.addKeyListener(this);才会开始监听,监听范围是整个大括号之内
然后是动作监听和timer,估计有的人会跟我一开始一样疑惑,不是有一个监听了吗?还要这个干什么,其实不然,键盘监听只能监听键盘按下松开的那一瞬间,而动作监听可以一直监听某个参数的状态或者某个条件是否满足,比如上面已经说到的isstart参数改变暂停继续,再比如说吃到食物蛇的长度加1,分数加1,这里没有动作监听就实现不了,有的人就好奇了这里也没有循环,凭啥能够一直监听啊,就是通过timer来实现的,先new一个timer,然后使用timer.start(间隔时间/毫秒,this)这里的this指的是插入ActionListener接口后重写的actionPerformed中的全部内容,我把间隔时间设为100,那么1秒内动作监听执行了10次,任何参数的变化和某条件是否满足都会被捕捉到
public void actionPerformed(ActionEvent e) {
if(isstart==false){//通过isstart参数是否为false使蛇移动
for (int i = length-1; i >0 ; i--) {
x[i]=x[i-1];
y[i]=y[i-1]; //System.out.println("执行了换位");
}if(x[0]==yx*30&&y[0]==yy*30)//蛇头的位置与原石的位置重叠,我就放一个ciallo
{
try {
new food().playMusic(c);c为装满ciallo音效地址的集合
四.关于蛇的移动
paint()与repaint()有的人可能会疑惑,Java虽然能画图,但图不是静止的吗?其实细想某些翻页小漫画或者说gif就知道了,只要每张画的动作较为连贯,且翻得足够快,在人眼看来就是动图,比如说王者120帧,其实就是手机图像一秒刷新了120次,所以帧数越高画面越流畅,看到这里大家就明白了前面的timer能够设置画面的刷新次数,但是不是说过只有paint()能画画吗?画面怎么更新的?这里是因为在动作监听里塞入了大量的repaint(),repaint()会执行一次类里面的paint()方法,那么在随着动作监听不断执行,里面蛇的身体坐标不断改变,画面也被不断重绘,虽然只有一秒十次,画面会有点闪闪的,不过还算流畅,所以有想法的朋友可以通过改变timer的值和调整蛇的速度来改变蛇的帧率
switch (head){
case "d": /*System.out.println("执行了头的移动");*/x[0]=x[0]+30;
if(x[0]==600){x[0]=0;}repaint();break;
case "a": x[0]=x[0]-30;if(x[0]==-30){x[0]=570;}repaint();break;
case "w": y[0]=y[0]-30;if(y[0]==0){y[0]=570;}repaint();break;
case "s": y[0]=y[0]+30;if(y[0]==600){y[0]=30;};repaint();break;
//在动作监听中监听参数head,使头不断移动,
for (int i = length-1; i >0 ; i--) {
x[i]=x[i-1];
y[i]=y[i-1]; //每一节都往前交换位置
//可以看到坐标不断改变,repaint()也不断执行,使画面开始运动
五.蛇的边界,吃食物,咬到自己
边界:虽然前面已经实现了蛇的移动,但运行下蛇会跑出框架消失,所以当头触碰到边界时,我们把头的坐标修改成另外一边就行了,这样看上去就像从另一端出来,坐标点的位置实际上是图片的左上角,关于边界为什么这么写,后面有解释。这段代码在上面
吃食物:首先食物需要随机刷新,且不能刷在与身体重叠的地方,这个很好解决,造一个生成食物的循环,循环条件为食物存在参数exist,while里面放一个对身体坐标不断遍历跟食物坐标进行比较的方法并返回布尔值用if包起来,一旦有相同的,返回false,重新生成食物坐标,继续比较,当食物位置正确时方法就return true,使if中的break执行,退出循环
yx = rand.nextInt(20);
yy = rand.nextInt(19)+1;//食物不存在时开始循环
while(exist==false){if(new food().test(x,y,yx*30,yy*30,length)==true){
exist=true;repaint();break;} yx = rand.nextInt(20);
yy = rand.nextInt(19)+1;//如果刷新位置不对就会重新随机
}//test方法为遍历比较
public boolean test(int a[],int b[],int o,int d,int c){
for(int i = 0;i < c;i++)
{if(a[i]==o&&b[i]==d){
System.out.println("重叠了");
return false;
}} return true;
}//直到返回true才会结束循环,这样就保证了食物位置的正确刷新
//这里建议大家把坐标什么的一步到位,我当时食物重叠位置条件没有乘上30,导致吃食物一直吃不到,后面足足找了我两个小时才找到原因,当时还以为电脑赛博闹鬼了
吃到食物使长度length+1
//创造派蒙蛇
//派蒙身体
for (int i = 1; i <length ; i++) {
new ImageIcon("图片/纠缠之缘.jpg").paintIcon(this, g, x[i], y[i]);
身体长度我用length来表示,初始为3,以后吃到原石了就加1
} //派蒙头
switch (head){case "d": new ImageIcon("图片/派蒙右.jpg").paintIcon(this, g, x[0], y[0]);break;
case "a": new ImageIcon("图片/派蒙左.jpg").paintIcon(this, g, x[0], y[0]);break;
case "w": new ImageIcon("图片/派蒙上.jpg").paintIcon(this, g, x[0], y[0]);break;
case "s": new ImageIcon("图片/派蒙下.jpg").paintIcon(this, g, x[0], y[0]);break;}//由于蛇在转向的时候头会转,所以得有四张头的照片,参数head代表头的方向,我用常玩的电脑awsd来表示,head不知何用的请继续看后面,涉及到监听
if(exist==true){
new ImageIcon("图片/原石.png").paintIcon(this, g, 30*yx, 30*yy); }
yx,yy是我随机出的两个整数,*30用来表示其刷新的位置
可以看到身体创造是从1到length-1,那么随着length的不断变大,身体也就自动变长了
咬到自己:该逻辑跟吃食物是一样的,即遍历身体坐标看有没有跟头的坐标重叠,如果重叠了就暂停(调整isstart=true)并把布尔值fail从false变为true,同时弹出失败的提示。一切关于按键的方法一定要写到键盘监听中,我当时把isstart和fail的if条件写在外面,手动在方法体中更改isstart为true,结果蛇就不再运动,原因是写在外面时当fail条件满足会将isstart值锁定,这时再怎么按空格键isstart也不发生改变。
当我们再次按下空格键的时候,游戏就重新开始
for (int i = 1; i < length; i++) { if(x[0]==x[i]&&y[0]==y[i]){//判断死亡条件
System.out.println("失败了");
fail=true;
isstart=true;
//等待重新开始不能用下面,因为上方代码先执行isistart会被锁定
永远到不了下面那个if里面了
// if(isstart==false){
// fail=false;this.unit();}
if(keyCode==KeyEvent.VK_SPACE){if(isstart==true&&fail==true){this.unit();}
//写在键盘监听中
//unit为初始化方法,执行后所有的参数回到初始状态
public void unit(){
x[0]=90;
y[0]=30;
x[1]=60;
y[1]=30;
x[2]=30;
y[2]=30;
for (int j = 3; j <max+3 ; j++) {x[j]=0;y[j]=0;}
length=3;head="d";fail=false;
}
//这里有两个细节,我在unit()中使用遍历,将身体以外的坐标都设为0,0这是因为由于游戏是重新开始的,前面三段是回来了,但随着吃下原石length的增加,上一把身体坐标会复活,如果不藏到0,0,画面中上把的身体图片由于坐标相差较大,在我们眼中会出现闪烁的情况
为什么我说是藏呢,打开框架窗口,实际上位置从窗边就开始了,那么就算会闪也被窗口盖住了,完美解决
那么,回过头去看看边界条件那段代码,就知道为什么坐标要那么写了
六.背景音乐和奖励音效ciallo
由于本人比较喜欢整活,不仅加入了背景音乐,派蒙蛇在吃到原石的时候还会发出ciaollo的声音,问就是我其实喜欢玩galgame。(大家调试的时候记得戴耳机,外放很尴尬)那么只需要写出播放音乐的方法,在动作监听下满足吃到原石条件我就调用一次这个方法,至于背景音乐在主方法里面调用,方便一直播放。大家有自己喜欢的音效可以放进去。
File a=new File(path);//path为音频位置
AudioInputStream ais=AudioSystem.getAudioInputStream(a);读取电脑能看得懂的音乐
Clip clip = AudioSystem.getClip();//clip可以理解为音乐的一个容器
clip.open(ais); //放入音乐
clip.start();//容器开始播放
//记得要抛出异常
//在bgm处在open后面用到的是clip.loop(次数)Clip.LOOP_CONTINUOUSLY为无限循环
我依旧将isstart与bgm的播放绑定
while(true){ if(window.isstart==false)//在调用处我手动监听
{ clip.loop(Clip.LOOP_CONTINUOUSLY);
}if(window.isstart==true){clip.stop(); }}//停下,这一句是必要的,
由于初始时isstart为true,如果不用if将其锁定住,clip进程貌似会自动关闭,音乐没有播放
那么这样一来背景音乐也会随着空格键开关
需要注意的一点是java只支持wav等格式,不能在文件夹里改后缀,他还是会报错unsupport什么的, 解决方法是在网上随便找一个在线音频编辑器保存成另一个格式
可以用这个音频剪辑器 [HQ] — 在线音频编辑器
七.个性化的界面,计数板,失败特效
可以看到我在右边贴上了甘雨和神子的图,这个很好实现,依旧用的是前面讲到的imageicon,只要调整好位置就行了,以图片左上角坐标为准,除了图片以外,我们也要加入必要的文本,目前我知道的就是paint()中的g.drawString("文本",x坐标,y坐标);用这就可以在图片上写字和记录分数了,不过注意他的声明一定要在图片的后面,不然会被盖住。paint()中图层越后画的越在上面
Font zi1 = new Font("“宋体”",Font.BOLD,20);//字体的声明
字体类型还有很多,不止宋体,可以去查一下
g.setColor(Color.white);
g.setFont(zi1);//此为调整g的字体
g.drawString("按压空格开始或暂停",260,300);
g.drawString("分数:"+score,627,170);//此为计数板,打印score
用变量score记录吃到的个数
那么失败特效就很好理解了,在paint()中加入对fail值的判断,一旦fail值为true,就输出失败的图片和文本提示
if(fail==true){
new ImageIcon("图片/受伤.jpg").paintIcon(this, g, x[0], y[0]);
new ImageIcon("图片/失败.jpg").paintIcon(this, g, 0, 150);
g.setColor(Color.orange);
g.drawString("再起不能,",120,250);
g.setColor(Color.blue);
g.drawString("派蒙不能作为应急食物",120,300);
g.setColor(Color.GREEN);
g.drawString("按压空格重新开始",120,350);
}
八.最高纪录的读取
我们每把游戏都把分数赋给了score,那么完全可以使用io流将最高分给保存下来,启动游戏的时候读取最大值max,一旦score超过了max,将max重新赋值,并写入文本文档中。
public void file(){
//存档位置
try { FileWriter writer = new FileWriter(filePath);//这里的filepath为文本文件的地址
writer.write(score+"");
//后面的参数只能是字符串或者对应的ascll码,于是score加一个空的“”进行存储
writer.flush();//必要的,将数据输出,没有这句就不会写
} catch (IOException e) {
throw new RuntimeException(e);
}
}public void read(){//读取存档
StringBuilder sb = null;
try {
FileInputStream reader = new FileInputStream(filePath);
DataInputStream ad = new DataInputStream(reader);
sb = new StringBuilder();
int c;
while ((c = ad.read()) != -1) {
sb.append((char) c);}
} catch (IOException e) {
throw new RuntimeException(e);
}
String loadedData = sb.toString();
int temp=Integer.parseInt(loadedData);
max=temp;}
那么,在初始化的时候往里加一个read(),在动作监听那里加一个if判断score是否大于max,在if的方法体中加一个file()方法,那么最大值的存档就实现了,需要注意的是,我们一开始需要往空的文本文件里写个0或什么数字,不然初始化读的时候读不到就会报错,从这我们知道所有软件的参数一定是保存在本地的某个位置的,找到的话可以改动我们想要的数据。
九.经验的总结
做这个小项目途中遇到了各种各样的问题,耗时五天把整个代码和这篇文章搞了出来,在做的途中往往需要在一些功能运行不通的地方花上好几个小时,以下是我的收获到的一些经验:
1.在运行不同的地方可以放一个sout,看看代码是否真的执行了
2.在报错时,不断的向前注释掉新加入的内容,就可以找到到底是哪有问题
3.现在的ai不怎么管用,问的代码很多地方都不说全,不过可以向他问一下一些类的基本方法
如果你有什么不懂的地方,可以向我提问哦,我看到了就会回答,如果这篇文章对你有帮助,希望你能给个赞支持一下