这次这个案例可以说是头几次所讲的内容的一个技术汇总,主要是 运用了几大块的知识。我们先来定义一下案例的背景:在一个300*400的窗体上,有10个随机产生的字母下落,在键盘上敲击字母,若是敲对了就消掉,初始化的成绩为1000分,每次敲对一个字母就加上10分,如果在字母落到了屏幕的下方还没有敲对的话则判定为失败,就扣除100分。
我们还是老样子,先来进行步骤的划分
1.做满天星
2.把星星改成随机的10个字母
3.让字母落下,如果字母落出了屏幕就生成新的字母,并从屏幕的上方重新出现
4.接收键盘输入并消除匹配的字母
5.实现积分程序
6.暂时先不说吧,先把第六步完成了,自然而然就能看到这一步需要什么了
## **1.做满天星** ##
现在做的星星,有些条件有所改变,需要一个300*400的窗体,需要10颗星星,现在大家先自己写一遍代码,随后再看看我的代码
import java.awt.*;
public class MyChar {
public static void main(String[] args) {
// TODO Auto-generated method stub
Frame w = new Frame();
w.setSize(300, 400);
MyPanel mp = new MyPanel();
w.add(mp);
w.show();
}
}
class MyPanel extends Panel
{
public void paint(Graphics g)
{
for(int i=0;i<10;i++)
{
g.drawString("*", (int)(Math.random()*300), (int)(Math.random()*300));
}
}
}
这里有几个地方需要注意一下,窗体的背景不再是黑色的,那么字体的颜色也不用再设置成白色的了,黑色的部分可能是个意外,为什么在纵坐标上我们不乘上400?这个问题肯定很多人都会想到,这里我来告诉各位,你想想我们的最终目的是让字母下落,等着用户看到键盘上对应的键,如果乘上400,就有可能有些字母一出来就在屏幕的最下方,这貌似并不符合我们设计的游戏的逻辑。事实上,我们前面的经验表明,如果乘上400,有些字母一出现可能会因为数值太大而看不见。
第二步是生成10个随机字母,我们索性将这些在这一步将坐标放到数组变量里,以便准备给下落动作使用。
问题是,随机的字母如何产生?我们从未讲过数据类型,我认为一上来就介绍8中基本数据类型毫无意义,如果没有用到就会很快的忘记,回顾一下我们已经使用的数据类型都有什么。毫无疑问,第一个想到的就是int,整数类型(简称整型),这是目前为止我们使用最多的数据类型,既然有整型那么就一定有小数型,在很多语言里都有两张表达小护士的数据类型,一个float,另一个是double。看到单词我们就能知道个大概,double是两个的意思,这是由计算机的特点决定的,double用的两倍的空间来存储数据,所以可以想象的到double可以表示更多的数字,具体到小数来看,结果就是更加的精确,但是也就更加的浪费资源。事实上,整型也有short和long之分,这个也很好理解,从字面上去解释,short就是短的意思,对应的就是短整型,long是长的意思,对应的就是长整型,两者的区别就是表示的整型数据的长度不同,short int所表示的数据范围是 —32768to+32767。long int 表示的是-2,147,483,648 to 2,147,483,647 这样一来就很直观了嘛,long所表示的范围比short大的多得多
言归正传,另外,我们还用到了boolean类型,你说我没讲到boolean,其实我们在if或者循环里用做条件的表达式结果,boolean简单,里面就两个可能的值:true or false。
还有一个是byte类型,因为计算机是用0和1最为基本的运算单位的,但是表达的东西太少了,所以人们将0和1组成一个组来存放稍大一点的数,8个二进制数组合起来就是一个byte,这成为编程中最常用的基本数值单位。
介绍到我们关注的一个数据类型,即char。char是字符类型,就是放字符的,字符用单引号引起来,比如'a',这个我们没有用过,之前用双引号"*"不算,因为这是字符串String,这个String不放在数据类型里讨论。String是对象不是基本数据类型,这句话意味着这里讨论的数据类型不是对象,你可能觉得不是就不是呗,可是从学术的角度或者一直面向对象的程序员看来,就难以适应了,有人抨击Java不是纯粹的面向对象语言,指的就是8中基本数据类型不是对象,从程序员的角度来看,不是对象意味着点点不会出现方法。有的时候这是个问题,为什么Java的设计者不能再纯粹一点呢?这是有原因的,设计者们看的很深很远,从效率的角度考虑,基本数据类型的操作比对象更有效率,不过为了照顾面向对象程序员,Java提供了基本数据类型的封装类,帮助我们在需要的时候将这个数变成对象。
为什么要搞这么多的数据类型呢?一是为了更好的使用内存,声明变量的时候如果指定了这个变量是boolean类型的,程序就不用为这个变量准备int类型那么大的空间了;而是不同数据类型之间的运算规则又是不同的,我们来看下面两个程序的运算结果。
public class MyTest {
public static void main(String[]args){
char a='1';
char b='2';
System.out.println(a+b);
}
}
运行一下你会发现得到了一个意想不到的数字99,再看看下面这段相似的代码
public class MyTest {
public static void main(String[]args){
char a=1;
char b=2;
System.out.println(a+b);
}
}
这段代码我想就算不允许也可以猜出结果是3了,为什么会有这样的差异,留给各位以后去探索吧
既然有不同的数据类型,就会遇到不同数据类型之间转换的问题,其实我们之前已经使用过这样的转换。我们用(int)将随机数产生的小数转换成整数,这叫做强制类型转换。事实上也有隐含的转换,比如我将小数硬放到一个整形变量里,自然放不了,系统会自动帮我转换成整型再放。我们就立即遇到一结果问题,既然类型不同,转换之后会成为什么呢?这里面一定会有比较合理的规则,比如,我们之前将小数转换成证书会谁去掉小数部分,我不需要告诉你所有的转换规则,在知道了各种数据类型,以及如何转换的情况下,你完全可以自己试一下,大多数情况下,一看结果就知道规则是什么了。
后续还会在需要的时候继续讨论数据类型这个话题,现在再多说想必也是吸收不进去了。
我们来尝试一下char和int的相互转换
public class MyTest {
public static void main(String[]args){
char a='a';
System.out.println((int)a);
}
}
```
运行结果是97,这是'a'这个字符在计算机里的编码,为了统一,这样的编码已经成了统一,后来成了国际标准也就是后来的ASCII编码,那么再来一段代码
public class MyTest {
public static void main(String[]args){
char a=’a’;
System.out.println((char)a);
}
}
结果很明显吧,一看就能猜出来是a,结果也确实是a,还记得我们的任务是什么?这个阶段的任务就是随机生成字母,也就是说,生成从a到z的随机字母,我们看到a对应的ASCII码是97,那么相应的z就是97+26.,如果能够生成97到97+26的随机数。我们能够通过强制类型转换获得随机字母,如何生成97到97+26之间的随机数呢?要知道Math.random()产生的0到1之间的随机数,我们需要转换,我想有人知道怎么做了——(char)(Math.random()*26+97)。
事实上,这边还有一个问题待解决。g.drawString()方法需要三个参数,这是我们知道的,第一个参数是一个字符串(String),第二个和第三个分别是X和Y坐标,是整数(int),我们的问题是,得到了随机字符,可是那是字符不是字符串,字符没法满足这个方法的需求,我们需要将字符强行转换为字符串。根据此前的经验,或许有人会用这种办法啦转换——(string)c,这里假设c是字符变量,这是不行的,这种做法只是适合基于数据类型的转换,还有一种情况也是用这种转换方式,不过现在不讲,后续会说明,我这儿有一个省事的方法,在需要转换的地方写" "+c,因为" "是一个完全空的字符串,要知道在大多数的情况下,一个变量加到字符串上,都会被自动转换成字符串,无论是基本数据类型还是对象,唯一的问题就是这样做的办法好像效率太低了,计算机需要更加长的时间去执行这个操作,字符串想叫的做法无疑会让这个程序的运行效率变得很低,所以如果是正式的编程,不建议这样做类型转换。正确的做法是把c变成对象——new Character(c),所有的对象都有to String()方法,我们用这个方法来转换得到字符串。这样就遗留了一个问题,为什么所有的对象都要有to String()方法呢?不急,后面我会说道的。
充分理解上面的讨论之后,你可以尝试一下完成这一步的代码,让300*400的窗体上有10个随机的字母。
import java.awt.*;
public class MyChar {
public static void main(String[] args) {
// TODO Auto-generated method stub
Frame w = new Frame();
w.setSize(300, 400);
MyPanel mp = new MyPanel();
w.add(mp);
w.show();
}
}
class MyPanel extends Panel
{
int x[]=new int[10];
int y[]=new int[10];
char c[] = new char[10];
MyPanel()
{
for(int i=0;i<10;i++)
{
x[i]=(int)(Math.random()*300);
y[i]=(int)(Math.random()*300);
c[i]=(char)(Math.random()*26+97)
}
}
public void paint(Graphics g)
{
for(int i=0;i<10;i++)
{
g.drawString(new Character(c[i]).toString(),x[i],y[i]);
}
}
}
## 3.让字母落下,如果字母落出了屏幕就生成新的字母,并从屏幕的上方重新出现 ##
如果这一步没有问题了,我们来完成第三步,及时让字母落下。如果字母落出屏幕就生成新的字母,并从屏幕的上方重新的出现。
落下的代码不难,几乎和下雪的代码一样,不同的是,下雪的代码在超出屏幕的处理上是让Y值回到0。为了有更好的效果,在这个案例上,我们还要让X获得一个新的随机数,另外这个字符也要产生一个字符。
同样建议你先自己试着完成任务,然后再看代码,如果没有思路,还是回头看看前面,看看还有哪里没有彻底的掌握。
import java.awt.*;
public class MyChar {
public static void main(String[] args) {
// TODO Auto-generated method stub
Frame w = new Frame();
w.setSize(300, 400);
MyPanel mp = new MyPanel();
w.add(mp);
Thread t = new Thread(mp);
t.start();
w.show();
}
}
class MyPanel extends Panel implements Runnable
{
int x[]=new int[10];
int y[]=new int[10];
char c[] = new char[10];
MyPanel()
{
for(int i=0;i<10;i++)
{
x[i]=(int)(Math.random()*300);
y[i]=(int)(Math.random()*300);
c[i]=(char)(Math.random()*26+97);
}
}
public void paint(Graphics g)
{
for(int i=0;i<10;i++)
{
g.drawString(new Character(c[i]).toString(),x[i],y[i]);
}
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true)
{
for(int i=0;i<10;i++)
{
y[i]++;
if(y[i]>400)
{
y[i]=0;
x[i]=(int)(Math.random()*300);
y[i]=(int)(Math.random()*26+97);
}
}
try
{
Thread.sleep(30);
}catch(Exception e){ }
repaint();
}
}
}
## 4.接收键盘输入并消除匹配的字母 ##
前三部基本都已经完成,现在做的是第4步,接收用户键盘的输入,如果匹配上就消除这个字符。事实上,我们并不真正的去消除字符,而是让这个字符重新从屏幕的上面再出来,这将是个新生成的字符。接收用户输入我想不是问题了,那么拿到用户的输入以后怎么知道屏幕上有没有的这个字符呢?字符都存在在那个数组里,看来我们得到数组里去找有没有,如果有就处理。
还是自己先去尝试一下再看一下代码,不过这一步我不在提供所有的代码,只是部分,我只是觉得大家已经有能力看得懂了,所以不再和之前一样提供所有的代码了,下面只是列出了事件处理的程序
public void keyPressed(KeyEvent arg0)
{
//将用户输入的字符存入KeyC中
char keyC=arg0.getKeyChar();
//扫描整个数组,看看有没有匹配的字符
for(int i=0;i<10;i++)
{
if(keyC==c[i])
{
//找到了
y[i]=0;
x[i]=(int)(Math.random()*300);
c[i]=(char)(Math.random()*26+97);
break; //防止上同时有多个相同的字符被一次性的消除掉
}
}
}
break这里做一个说明,break的作用是跳出这个循环,在这里加上break的目的是如果找到了那么这次就不找了。
加入了上述的代码,别忘了要实现接口和注册事件。如果成功了,那么恭喜你,打字母的游戏已经出来了。不过你在调试的过程中也已经意识到了这个问题,就是如果有多个相同的字母同时出现,那么被消除掉的那个字母就不一定是最先面的那一个了,这貌似也是不符合这个游戏的逻辑,因为消除掉的顺序是在数组里。字母在数组里是按照什么样的顺序排列的,那么消除就是按照这样的顺序去消除,这就和y坐标没有关系了,大家也应该反应过来了这也就是我在前面留下的第六个问题,就是消除掉最下面的字母
## 5.实现积分程序 ##
现在还是先来看第五步,计入积分。我们似乎需要一个变量来记录得分,那么就根据案例的要求,就假设初始值就是1000,如果字母在落到最下面的时候还没有被消除掉那么就要扣除10分,如果用户输入的字符屏幕上没有那就扣除100分。自己想想写写看该怎么去完成
class MyPanel extends Panel implements Runnable
{
int x[]=new int[10];
int y[]=new int[10];
char c[] = new char[10];
int sorce=1000;
public void paint(Graphics g)
{
for(int i=0;i<10;i++)
{
g.drawString(new Character(c[i]).toString(),x[i],y[i]);
}
//显示成绩
g.setColor(Color.RED);
g.drawString(“你的成绩是:”+sorce, 5, 10);
}
}
下面的代码是实现扣分功能的代码,你看到"-="运算符了吧,相似的还有"+= *= /+"之类的,这里是score = score-1000的简单写法
public void run() {
// TODO Auto-generated method stub
while(true)
{
for(int i=0;i<10;i++)
{
y[i]++;
if(y[i]>400)
{
y[i]=0;
x[i]=(int)(Math.random()*300);
y[i]=(int)(Math.random()*26+97);
sorce-=100;
}
}
}
}
最后我们来看看如何实现敲对了加分的功能,敲错了减分的功能
public void keyPressed(KeyEvent arg0)
{
//将用户输入的字符存入KeyC中
char keyC=arg0.getKeyChar();
//扫描整个数组,看看有没有匹配的字符
for(int i=0;i<10;i++)
{
if(keyC==c[i])
{
//找到了
y[i]=0;
x[i]=(int)(Math.random()*300);
c[i]=(char)(Math.random()*26+97);
break; //防止上同时有多个相同的字符被一次性的消除掉
}
else
{
score-=100;
}
}
}
你觉得这样的代码可以吗?肯定不可以啊。在我们看来,假设数组里的10个字符中有一个'a',你却敲'a'这个字符,你希望的道德结果是加10分,但是最坏的结果是循环了10次,最后一个匹配上了。匹配上了这次虽然说是加了10分,但是前面没有匹配上的9次怎么办?扣900分吗?这样不符合逻辑啊,这是一个很经典的问题,语法上没有错误但是逻辑上存在问题,因此我们就需要一个标识,如果没有匹配上,标识的值不变,一旦匹配上了,标识改变。等到循环完了,看一下标识,有很多情况下我们都会用到这个小算法
具体的实现来看,boolean变量貌似是最适合的,因为boolean变量只有两种状态,我们先设定boolean变量的初值是false(假的),如果if匹配上课了,就把他变成true。
boolean mark =false;
for(int i=0;i<10;i++)
{
if(keyC==c[i])
{
mark=true;
break;
}
}
我们来分析一下上面的代码,开始mark的值是false,然后循环,如果这10次循环都没有if成功,那么mark的值就会一直不改变,还是false,一旦if匹配成功,mark的值就变成了true,我们可以在循环结束后,更具mark的值来判断有没有匹配上的内容。
具体到了案例的实现代码
public void keyPressed(KeyEvent arg0)
{
//将用户输入的字符存入KeyC中
char keyC=arg0.getKeyChar();
//扫描整个数组,看看有没有匹配的字符
boolean mark =false;
for(int i=0;i<10;i++)
{
if(keyC==c[i])
{
//找到了
y[i]=0;
x[i]=(int)(Math.random()*300);
c[i]=(char)(Math.random()*26+97);
mark=true;
break; //防止上同时有多个相同的字符被一次性的消除掉
}
}
if(mark)
{
score+=100;
}
else
{
score+=10;
}
}
这次将的东西我也承认确实是有那么一点点的多,我建议大家还是先停一下,把前面的代码再自己多写写,多多理解理解再继续下去,因为最后一步的逻辑有点棘手,不是很好理解
## 6.如何消除最下面的字符 ##
现在开始完成第6步,我们的目的是找到多个匹配成功的字符的最下面一个,并且清除掉,其中清除不难,找到匹配的字符也不难,焦点在最下面一个字符上,如何判断这是最先面的自字符?这很简单,就是Y坐标最大的。如果不是计算机来完成这件事情,让人来做,那就是找到所以的匹配字符,摆在那里,看看谁的Y最大,计算机没有一次性比较多个值的能力,每次只能比较出一个值,我们的思路是,先找到一个以后,然后把Y值记录下来,再找到一个,再判断,如果新的Y值大于原来的,那么就用这个Y值替换掉原来的Y值,否则什么都不做,老的字符在下面,找一圈以后,记录下的Y值是最大的了。我们可以理解这个逻辑,我拿到了一个字符,记住了它的Y坐标,在拿到一个看看是不是大于刚才那个,如果是就将旧的Y给丢了,如果不是就把现在这个新的Y个妞丢了,最后我手里就一个字符,这个字符就在最下面,它对应的数据位置就是我们要清除的字符标号,我们将数组位置叫做数组下标,也就是说每次我们还得记录保留下来的数组下标。
还有一个小问题,即便有很多匹配的字符,判断的逻辑是相同的,我们完全可以将这个判断放在循环里,让这个逻辑周而复始的去做,可是第一个字符的匹配逻辑不同,它不需要判断,见到存下来就好,能不能将第一个字符的逻辑和其他逻辑统一呢?如果能的话,我们就可以节省下一段代码了,我想到了一个办法,就存放在最下面Y坐标的变量初始值设置成绝对不可能小的数,这样第一个字符就去判断,当然,由于老的值一定小,所以第一个字符的值也会被存下来。
public void keyPressed(KeyEvent arg0)
{
//将用户输入的字符存入KeyC中
char keyC=arg0.getKeyChar();
//扫描整个数组,看看有没有匹配的字符
int nowY=-1;
int nowIndex=-1;
for(int i=0;i<10;i++)
{
if(keyC==c[i])
{
if(y[i]>nowY)
{
nowY=y[i];
nowIndex=i;
}
}
}
if(nowIndex!=-1)
{
y[nowIndex]=0;
x[nowIndex]=(int)(Math,random()*300);
c[nowIndex]=(char)(Math.random()*26+97);
score+=10;
}
else
{
score-=10;
}
}
我们来看看上面这段代码,nowY存放着最下面符合条件的Y坐标,nowIndex存放着最下面符合条件的数组的下标,boolean变量似乎也不需要了,因为完全可以根据nowIndex来判断有没有找到,break也不需要了,因为我们找到一个匹配的字符不算完,还要继续寻找,最后我放上所有的代码
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public class MyChar {
public static void main(String[] args) {
// TODO Auto-generated method stub
Frame w = new Frame();
w.setSize(300, 400);
MyPanel mp = new MyPanel();
w.add(mp);
Thread t = new Thread(mp);
t.start();
w.addKeyListener(mp);
mp.addKeyListener(mp);
w.show();
}
}
class MyPanel extends Panel implements Runnable,KeyListener
{
int x[]=new int[10];
int y[]=new int[10];
char c[] = new char[10];
int score=1000;
MyPanel()
{
for(int i=0;i<10;i++)
{
x[i]=(int)(Math.random()*300);
y[i]=(int)(Math.random()*300);
c[i]=(char)(Math.random()*26+97);
}
}
public void paint(Graphics g)
{
for(int i=0;i<10;i++)
{
g.drawString(new Character(c[i]).toString(),x[i],y[i]);
}
//显示成绩
g.setColor(Color.RED);
g.drawString(“你的成绩是:”+score, 5, 10);
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true)
{
for(int i=0;i<10;i++)
{
y[i]++;
if(y[i]>400)
{
y[i]=0;
x[i]=(int)(Math.random()*300);
y[i]=(int)(Math.random()*26+97);
score-=100;
}
}
try
{
Thread.sleep(30);
}catch(Exception e){ }
repaint();
}
}
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
//将用户输入的字符存入KeyC中
char keyC=e.getKeyChar();
//扫描整个数组,看看有没有匹配的字符
int nowY=-1;
int nowIndex=-1;
for(int i=0;i<10;i++)
{
if(keyC==c[i])
{
if(y[i]>nowY)
{
nowY=y[i];
nowIndex=i;
}
}
}
if(nowIndex!=-1)
{
y[nowIndex]=0;
x[nowIndex]=(int)(Math.random()*300);
c[nowIndex]=(char)(Math.random()*26+97);
score+=10;
}
else
{
score-=10;
}
}
@Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
}
}
“`
这应该是目前博主写博客以来写的最多的一次,当然你们看的学的觉得很辛苦,博主自己写的也很辛苦,好好的而消化,多练几遍,要轻松的理解这些东西并非易事,良好的逻辑能力需要培养,亲自写代码,自己改错,非常锻炼编程能力,坚持不懈的努力,终究会成功,这次就到这里了,下期再见了,下期更完图形界面应该是告一段落了,要真的开始写项目了。