1. 程式人生 > >Java圖形介面實戰案例——實現打字母遊戲

Java圖形介面實戰案例——實現打字母遊戲

實現打字母的遊戲

 這次這個案例可以說是頭幾次所講的內容的一個技術彙總,主要是 運用了幾大塊的知識。我們先來定義一下案例的背景:在一個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,有些字母一出現可能會因為數值太大而看不見。

2.把星星改成隨機的10個字母

第二步是生成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.,如果能夠生成9797+26的隨機數。我們能夠通過強制型別轉換獲得隨機字母,如何生成9797+26之間的隨機數呢?要知道Math.random()產生的01之間的隨機數,我們需要轉換,我想有人知道怎麼做了——(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

}

}

“`
這應該是目前博主寫部落格以來寫的最多的一次,當然你們看的學的覺得很辛苦,博主自己寫的也很辛苦,好好的而消化,多練幾遍,要輕鬆的理解這些東西並非易事,良好的邏輯能力需要培養,親自寫程式碼,自己改錯,非常鍛鍊程式設計能力,堅持不懈的努力,終究會成功,這次就到這裡了,下期再見了,下期更完圖形介面應該是告一段落了,要真的開始寫專案了。