3. 游戲的開發與編碼
在我們開發這個游戲之前,我們先講一個這個游戲的實現所采用的方法,那就是經典的MVC模式,因為在開發游戲的時候,結構很重要,必須要理清楚每一塊負責什么,每一個類負責什么,而MVC模式正好就是解決這種問題的很好的方案,我們可以把游戲的運行流程交由一個類去統一調度,游戲的呈現也就是繪圖用專門一個類去負責,而繪圖所需的數據可以從一個模型類里面去取,控制的類負責更改模型里面的數據并調用視圖類去更新當前的視頻,這樣整個游戲的流程就很清晰明了。所以我們設計了如下幾個類,它們之間互相交互,形成整個游戲的框架。
1,ClientControl
顧名思義,這個類就是我們的控制端類,它負現整個游戲的流程控制以及事件處理。它在MVC里面的角色是C。
2,ClientModel
它就是我們程序運行的時候,放數據的地方,它存放的數據并不是一般的數據,而是需要雙方一起交互的數據,它只是做為一個橋梁,連接控制端和視圖端的紐帶。它是MVC的角色里面是M.。
3,ClientView
它是我們今天需要重點講解的地方,它是我們MVC里面的視圖的實現,它負責呈現整個游戲的界面,并且它也受控制端ClientControl的支配,由ClientControl請求它重繪。它重繪的時候,一些數據將從ClientModel里面去取。
那么我們重點來看一看ClientView的代碼:
* ClientView.java
*
* Created on 2007年10月2日, 下午2:00
* 此類專門負責視圖的實現,此類中須定義從模型中
* 取出數據并重繪的方法
* To change this template, choose Tools | Template Manager
* and open the template in the editor.
*/
package com.hadeslee.apple.client;
import com.hadeslee.apple.common.Bet;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import java.util.Vector;
/**
*
* @author lbf
*/
public class ClientView extends JPanel {
private ClientModel cm; //模型類的一個對象
private volatile boolean isStar;
private Image starA; //表示當前星星的圖片
private Image[] star; //星星的數組
private int x1;
private int y1; //星星的座標
private Image bg2; //表示底襯的那層
private Image ratio; //賠率底襯的那層
private int x;
private int length; //表示跑馬燈的位置
/** Creates a new instance of ClientView */
public ClientView(ClientModel cm) {
this.cm = cm;
initOther();
x = 646;
new RunStar().start();
new Draw().start();
}
//初始化視圖類的一些參數
private void initOther() {
try {
star = new Image[3];
MediaTracker mt = new MediaTracker(this);
for (int i = 0; i < 3; i++) {
star[i] = Toolkit.getDefaultToolkit().createImage(this.getClass().getResource("pic/game/star/" + (i + 1) + ".png"));
mt.addImage(star[i], i);
}
bg2 = Toolkit.getDefaultToolkit().createImage(this.getClass().getResource("pic/game/bg2.png"));
ratio = Toolkit.getDefaultToolkit().createImage(this.getClass().getResource("pic/game/ratio.png"));
mt.addImage(bg2, 4);
mt.addImage(ratio, 5);
mt.waitForAll();
starA = star[0];
//把默認的鼠標改成我們自定義的鼠標形式,以配合主題
Image icon = Toolkit.getDefaultToolkit().createImage(this.getClass().getResource("pic/login/icon.png"));
Cursor cu = Toolkit.getDefaultToolkit().createCustomCursor(icon, new Point(0, 0), "my");
this.setCursor(cu);
} catch (InterruptedException ex) {
Logger.getLogger(ClientView.class.getName()).log(Level.SEVERE, null, ex);
}
}
//覆蓋的方法
protected void paintComponent(Graphics g) {
//先調用父類的方法,清除以前畫的內容
super.paintComponent(g);
//然后設置一些提示,比如屏幕抗鋸齒,以及文字抗鋸齒
Graphics2D gd = (Graphics2D) g;
gd.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
gd.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
//畫背景
g.drawImage(cm.getBg(), 0, 0, this);
g.drawImage(bg2, 0, 0, this); //再畫第二個背景
//畫賠率的閃動
if (cm.isRunning()) {
drawRatio(g);
}
//再畫桌面
drawTable(g);
//如果現在正在跑,那么就調用跑的那個方法
if (cm.isRunning()) {
drawRunning(g);
}
//如果現在正在賭剪刀石頭布,那么就調用比倍的方法
if (cm.isBetting()) {
drawBetting(g);
}
//畫出永遠存在的星星
if (isStar) {
g.drawImage(starA, x1, y1, this);
}
//畫出跑馬燈,提示文字
drawTip(g);
}
private void drawTip(Graphics g) {
g.setFont(new Font("宋體", Font.PLAIN, 22));
g.setColor(Color.RED);
g.drawString(cm.getInfo().getTip(), x, 25);
FontMetrics fm = g.getFontMetrics();
length = (int) fm.getStringBounds(cm.getInfo().getTip(),g).getWidth();
}
//畫出賠率
private void drawRatio(Graphics g) {
RatioA ra = cm.getRa();
RatioB rb = cm.getRb();
if (ra != null) {
g.drawImage(ratio, ra.x, ra.y, this);
}
if (rb != null) {
g.drawImage(ratio, rb.x, rb.y, this);
}
}
//畫出正在跑的方法
private void drawRunning(Graphics g) {
Vector<PP> ps = cm.getP();
for (PP p : ps) {
g.drawImage(p.current, p.x, p.y, this);
}
}
//畫出跑完的方法
private void drawRunOver(Graphics g) {
Vector<PP> ps = cm.getP();
for (PP p : ps) {
g.drawImage(p.current, p.x, p.y, this);
}
}
//畫出正在比倍的方法
private void drawBetting(Graphics g) {
g.drawImage(cm.getPKBG(), 172, 39, this);
g.drawImage(cm.getPkA(), 267, 245, this);
g.drawImage(cm.getPkB(), 386, 247, this);
}
//畫桌面以及桌面上的一些信息
private void drawTable(Graphics g) {
g.drawImage(cm.getTable(), 0, 0, this);
drawMoney(g);
drawBet(g);
}
//畫下注的那九格下注數字
private void drawBet(Graphics g) {
Bet b = cm.getBet();
drawNumber(80, 570, 12, 19, b.getBet(1), g, b.getWin(1));
drawNumber(183, 570, 12, 19, b.getBet(2), g, b.getWin(2));
drawNumber(252, 570, 12, 19, b.getBet(3), g, b.getWin(3));
drawNumber(318, 570, 12, 19, b.getBet(4), g, b.getWin(4));
drawNumber(424, 570, 12, 19, b.getBet(5), g, b.getWin(5));
drawNumber(527, 570, 12, 19, b.getBet(6), g, b.getWin(6));
drawNumber(597, 570, 12, 19, b.getBet(7), g, b.getWin(7));
drawNumber(664, 570, 12, 19, b.getBet(8), g, b.getWin(8));
drawNumber(767, 570, 12, 19, b.getBet(9), g, b.getWin(9));
}
//畫有余額,贏的錢,用戶ID,大小彩金等的方法
private void drawMoney(Graphics g) {
//畫余額和贏的錢
int allMoney = cm.getAllMoney();
int winMoney = cm.getWinMoney();
if (allMoney < 10000) {
drawNumber(762, 88, 24, 38, allMoney, g);
} else {
drawNumber(762, 94, 18, 28, allMoney, g);
}
if (winMoney < 10000) {
drawNumber(129, 86, 24, 38, winMoney, g);
} else {
drawNumber(129, 90, 18, 28, winMoney, g);
}
drawNumber(740, 208, 12, 19, cm.getId(), g); //畫ID號
//畫大彩金和小彩金
int smallBonus = cm.getInfo().getSmallBonus();
int bigBonus = cm.getInfo().getBigBonus();
if (smallBonus < 10000) {
drawNumber(760, 390, 24, 38, smallBonus, g);
} else {
drawNumber(760, 396, 18, 28, smallBonus, g);
}
if (bigBonus < 10000) {
drawNumber(128, 390, 24, 38, bigBonus, g);
} else {
drawNumber(128, 396, 18, 28, bigBonus, g);
}
}
//定義兩個重載的方法,分別針對于圖放大的圖片和一般大小的圖片
private void drawNumber(int startX, int startY, int num, Graphics g, boolean isWin) {
drawNumber(startX, startY, 24, 38, num, g, isWin);
}
private void drawNumber(int startX, int startY, int width, int height, int num, Graphics g) {
drawNumber(startX, startY, width, height, num, g, false);
}
private void drawNumber(int startX, int startY, int width, int height, int num, Graphics g, boolean isWin) {
String ns = Integer.toString(num);
int i = 0;
for (int start = ns.length() - 1; start >= 0; start--) {
i++;
char c = ns.charAt(start);
int index = c - 48;
if (isWin) {
g.drawImage(cm.getWinNumber(index), startX - (i * width), startY, width, height, this);
} else {
g.drawImage(cm.getNumber(index), startX - (i * width), startY, width, height, this);
}
}
}
//此類專門用于后臺調用重繪線程
private class Draw extends Thread {
public void run() {
while (true) {
try {
x -= 5;
if (x + length < 0) {
x = 800;
}
Thread.sleep(200);
repaint(x,0,length+20,30);
} catch (Exception exe) {
exe.printStackTrace();
}
}
}
}
//此類專門用于跑星星的閃動
private class RunStar extends Thread {
private int total;
public RunStar() {
isStar = true;
x1 = 339;
y1 = 106;
}
public void run() {
int index = 0;
while (true) {
try {
Thread.sleep(100);
if (index < star.length - 1) {
starA = star[++index];
} else {
starA = star[0];
index = 0;
total++;
}
if (total > 1) {
isStar = false;
repaint();
total = 0;
x1 = (int) (Math.random()*100-50) + 339;
y1 = (int) (Math.random()*100-50) + 106;
int sleep = (int) (Math.random()*3000) + 1000;
Thread.sleep(sleep);
isStar = true;
}else{
repaint(x1,y1,150,150);
}
} catch (Exception exe) {
exe.printStackTrace();
}
}
}
}
}
代碼其實不長,二百多行而已,我們先來看看如下幾個代碼片段:
Toolkit.getDefaultToolkit().createImage(this.getClass().getResource("pic/game/bg2.png"));
這句話有兩個需要我們注意的地方:
一是我們如何把圖片導入程序當中,二是我們如果把圖片打包進JAR包,然后如何得到它們的URL。
我們先講第一個,如何把圖片導入程序中,在這里我們用的是Toolkit的方法createImage,它確實是一個很實用的方法,它是一個重載的方法,可以傳入很多種參數,除了可以傳入URL之處,還可以有如下的重載方法:
|
|
|
|
|
|
|
|
|
|
有一點需要注意的是,它的createImage是一個異步的方法,也就是說我們調用了這個方法以后,程序會立即返回,并不會等到圖片完全加載進內存之后才返回,所以當我們用這種方法加載比較大的圖片的時候,如果圖片又沒有完全進入內存,而我們卻去draw它,這個時候就會出現撕裂的情況,大大影響了我們程序的性能以及可玩性,那怎么辦呢?
辦法有兩種,一種是像我們在程序里實現的一樣,用一個媒體跟蹤器來跟蹤我們要加載的圖片,然后調用一個同步方法等待它們全部加載進入內存之后才繼續往下運行,這樣就可以保存在初始化以后,所需要用到的圖片確實都全部加載進內存了,這樣畫的時候,才能保證效果。如下所示:
MediaTracker mt =
new MediaTracker(this);
…
mt.addImage(star[i],
i);
...
mt.waitForAll();
我們生成一個媒體跟蹤器,然后把我們需要跟蹤的圖片放到里面去,然后等待所有的圖片加載,mt.waitForAll()方法是會拋出一個InterruptedException的方法。我們需要捕獲處理它。
另外一種辦法就是利用javax.imageio.ImageIO的方法,它的read方法可以同步的把圖片完全讀入內存,某些情況下這是更方便的方法,因為使用它免去了加媒體跟蹤器的代碼。javax.imageio.ImageIO的read方法也有很多重載的版本,它的方法如下:
|
|
|
|
|
|
|
|
所以我們用read方法的話,會顯得更加方一些,但是為什么我們在程序當中不使用它,而使用再加繁瑣的Toolkit加上MediaTracker的方法呢?因為ImageIO讀入內存的圖片在呈現的過程中會有如下缺點:
1,當加載的圖片是動態的gif圖片的時候,圖片在呈現的時候,將沒有動畫效果,它只會讀取第一幀。
2,當加載的圖片是半透明的時候,圖片在呈現的時候,會比用Toolkit加載進來的圖片更耗CPU。
所以我們選擇了用Toolkit而不是ImageIO,當我們沒有用到以上兩種情況的圖片的時候,是完全可以用ImageIO來加載圖片的。
圖片導入程序中的問題解決了,我們現在來看一看如何把圖片打包進JAR包,然后又如何在程序運行的時候把JAR包里面的資源提取出來。在這里我們用的是一個很有用的方法getResource(),它是定義在Class類里面的,當我們把我們的的圖片提取出來的時候,可以用相對路徑也可以用絕對路來來提取,當我們用相對路徑的時候,路徑就是相對于當前的class文件所在目錄的路徑,如果是用絕對路徑的時候,路徑就是從JAR內部的根目錄開始算的。把圖片等一些資源打入JAR包有很多好處,一是可以實現資源的初步隱藏,二是可以利用JAR的特性對文件進行一些壓縮,因為JAR包就是一個壓縮包,只不過后綴名改了而已。
下面我們再來看一下paintComponent方法,它是一個重寫的方法,它重寫了父類JPanel里面的paintComponent方法,一般來說,當我們要繪制一些內容的時候,都是采用重寫此方法的辦法,在以前AWT的編程中,對重量型組件進行重寫,一般重寫的是paint方法,所以在用輕量級組件的時候,這一點要注意,最好不要再重寫paint方法了,而是改為重寫paintComponent。在它里面我們看到如下三句:
Graphics2D gd =
(Graphics2D) g;
gd.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
gd.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
它的意思就是設置圖形上下文在繪制的時候,要注意哪些方面,我們可以利用這個方法給圖形上下文一些提示。在本文里面我們提示了兩點,一點是圖片抗鋸齒打開,二是文本抗據齒我們也打開,除了這兩個之外還要很多提示我們可以設置的,在興趣的朋友可以查看java.awt.RenderingHints這個類。利用這個特性,我們可以使我們呈現的界面更加完美,不過完美是需要代價的,呈現的越清晰越完美就越需要更多的CPU的運算,所以當電腦的性能不太好的時候,我們可以把這兩個提示去掉,讓JVM自行把握繪制的質量。
還有一點我們要注意的地方,那就是我們調用repaint的地方。在我們需要重繪的時候,我們可以調用repaint方法,它會發送一個重繪的請求,那個會把這個請求放到重繪線程里面去,在我們調用repaint的時候,有很重要的一點就是盡量不要去調用repaint的默認方法,而要調用repaint(int
x,int y,int width,int height)方法,因為它只會請求重繪某一個區域,而repaint()則會重繪整個區域,所以為了性能著想,最好不要重繪整個區域,當你開發了有關JAVA2D的程序后,你會發現,程序的大部份CPU都耗在重繪上面,所以優化重繪區域對于優化整個程序的性能是很有效果的。
盡管千里冰封
依然擁有晴空
你我共同品味JAVA的濃香.