3. 游戲的開發(fā)與編碼
在我們開發(fā)這個(gè)游戲之前,我們先講一個(gè)這個(gè)游戲的實(shí)現(xiàn)所采用的方法,那就是經(jīng)典的MVC模式,因?yàn)樵陂_發(fā)游戲的時(shí)候,結(jié)構(gòu)很重要,必須要理清楚每一塊負(fù)責(zé)什么,每一個(gè)類負(fù)責(zé)什么,而MVC模式正好就是解決這種問題的很好的方案,我們可以把游戲的運(yùn)行流程交由一個(gè)類去統(tǒng)一調(diào)度,游戲的呈現(xiàn)也就是繪圖用專門一個(gè)類去負(fù)責(zé),而繪圖所需的數(shù)據(jù)可以從一個(gè)模型類里面去取,控制的類負(fù)責(zé)更改模型里面的數(shù)據(jù)并調(diào)用視圖類去更新當(dāng)前的視頻,這樣整個(gè)游戲的流程就很清晰明了。所以我們?cè)O(shè)計(jì)了如下幾個(gè)類,它們之間互相交互,形成整個(gè)游戲的框架。
1,ClientControl
顧名思義,這個(gè)類就是我們的控制端類,它負(fù)現(xiàn)整個(gè)游戲的流程控制以及事件處理。它在MVC里面的角色是C。
2,ClientModel
它就是我們程序運(yùn)行的時(shí)候,放數(shù)據(jù)的地方,它存放的數(shù)據(jù)并不是一般的數(shù)據(jù),而是需要雙方一起交互的數(shù)據(jù),它只是做為一個(gè)橋梁,連接控制端和視圖端的紐帶。它是MVC的角色里面是M.。
3,ClientView
它是我們今天需要重點(diǎn)講解的地方,它是我們MVC里面的視圖的實(shí)現(xiàn),它負(fù)責(zé)呈現(xiàn)整個(gè)游戲的界面,并且它也受控制端ClientControl的支配,由ClientControl請(qǐng)求它重繪。它重繪的時(shí)候,一些數(shù)據(jù)將從ClientModel里面去取。
那么我們重點(diǎn)來看一看ClientView的代碼:
* ClientView.java
*
* Created on 2007年10月2日, 下午2:00
* 此類專門負(fù)責(zé)視圖的實(shí)現(xiàn),此類中須定義從模型中
* 取出數(shù)據(jù)并重繪的方法
* 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; //模型類的一個(gè)對(duì)象
private volatile boolean isStar;
private Image starA; //表示當(dāng)前星星的圖片
private Image[] star; //星星的數(shù)組
private int x1;
private int y1; //星星的座標(biāo)
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();
}
//初始化視圖類的一些參數(shù)
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];
//把默認(rèn)的鼠標(biāo)改成我們自定義的鼠標(biāo)形式,以配合主題
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) {
//先調(diào)用父類的方法,清除以前畫的內(nèi)容
super.paintComponent(g);
//然后設(shè)置一些提示,比如屏幕抗鋸齒,以及文字抗鋸齒
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); //再畫第二個(gè)背景
//畫賠率的閃動(dòng)
if (cm.isRunning()) {
drawRatio(g);
}
//再畫桌面
drawTable(g);
//如果現(xiàn)在正在跑,那么就調(diào)用跑的那個(gè)方法
if (cm.isRunning()) {
drawRunning(g);
}
//如果現(xiàn)在正在賭剪刀石頭布,那么就調(diào)用比倍的方法
if (cm.isBetting()) {
drawBetting(g);
}
//畫出永遠(yuǎn)存在的星星
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);
}
//畫下注的那九格下注數(shù)字
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號(hào)
//畫大彩金和小彩金
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);
}
}
//定義兩個(gè)重載的方法,分別針對(duì)于圖放大的圖片和一般大小的圖片
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);
}
}
}
//此類專門用于后臺(tái)調(diào)用重繪線程
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();
}
}
}
}
//此類專門用于跑星星的閃動(dòng)
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();
}
}
}
}
}
代碼其實(shí)不長,二百多行而已,我們先來看看如下幾個(gè)代碼片段:
Toolkit.getDefaultToolkit().createImage(this.getClass().getResource("pic/game/bg2.png"));
這句話有兩個(gè)需要我們注意的地方:
一是我們?nèi)绾伟褕D片導(dǎo)入程序當(dāng)中,二是我們?nèi)绻褕D片打包進(jìn)JAR包,然后如何得到它們的URL。
我們先講第一個(gè),如何把圖片導(dǎo)入程序中,在這里我們用的是Toolkit的方法createImage,它確實(shí)是一個(gè)很實(shí)用的方法,它是一個(gè)重載的方法,可以傳入很多種參數(shù),除了可以傳入U(xiǎn)RL之處,還可以有如下的重載方法:
|
|
|
|
|
|
|
|
|
|
有一點(diǎn)需要注意的是,它的createImage是一個(gè)異步的方法,也就是說我們調(diào)用了這個(gè)方法以后,程序會(huì)立即返回,并不會(huì)等到圖片完全加載進(jìn)內(nèi)存之后才返回,所以當(dāng)我們用這種方法加載比較大的圖片的時(shí)候,如果圖片又沒有完全進(jìn)入內(nèi)存,而我們卻去draw它,這個(gè)時(shí)候就會(huì)出現(xiàn)撕裂的情況,大大影響了我們程序的性能以及可玩性,那怎么辦呢?
辦法有兩種,一種是像我們?cè)诔绦蚶飳?shí)現(xiàn)的一樣,用一個(gè)媒體跟蹤器來跟蹤我們要加載的圖片,然后調(diào)用一個(gè)同步方法等待它們?nèi)考虞d進(jìn)入內(nèi)存之后才繼續(xù)往下運(yùn)行,這樣就可以保存在初始化以后,所需要用到的圖片確實(shí)都全部加載進(jìn)內(nèi)存了,這樣畫的時(shí)候,才能保證效果。如下所示:
MediaTracker mt =
new MediaTracker(this);
…
mt.addImage(star[i],
i);
...
mt.waitForAll();
我們生成一個(gè)媒體跟蹤器,然后把我們需要跟蹤的圖片放到里面去,然后等待所有的圖片加載,mt.waitForAll()方法是會(huì)拋出一個(gè)InterruptedException的方法。我們需要捕獲處理它。
另外一種辦法就是利用javax.imageio.ImageIO的方法,它的read方法可以同步的把圖片完全讀入內(nèi)存,某些情況下這是更方便的方法,因?yàn)槭褂盟馊チ思用襟w跟蹤器的代碼。javax.imageio.ImageIO的read方法也有很多重載的版本,它的方法如下:
|
|
|
|
|
|
|
|
所以我們用read方法的話,會(huì)顯得更加方一些,但是為什么我們?cè)诔绦虍?dāng)中不使用它,而使用再加繁瑣的Toolkit加上MediaTracker的方法呢?因?yàn)镮mageIO讀入內(nèi)存的圖片在呈現(xiàn)的過程中會(huì)有如下缺點(diǎn):
1,當(dāng)加載的圖片是動(dòng)態(tài)的gif圖片的時(shí)候,圖片在呈現(xiàn)的時(shí)候,將沒有動(dòng)畫效果,它只會(huì)讀取第一幀。
2,當(dāng)加載的圖片是半透明的時(shí)候,圖片在呈現(xiàn)的時(shí)候,會(huì)比用Toolkit加載進(jìn)來的圖片更耗CPU。
所以我們選擇了用Toolkit而不是ImageIO,當(dāng)我們沒有用到以上兩種情況的圖片的時(shí)候,是完全可以用ImageIO來加載圖片的。
圖片導(dǎo)入程序中的問題解決了,我們現(xiàn)在來看一看如何把圖片打包進(jìn)JAR包,然后又如何在程序運(yùn)行的時(shí)候把JAR包里面的資源提取出來。在這里我們用的是一個(gè)很有用的方法getResource(),它是定義在Class類里面的,當(dāng)我們把我們的的圖片提取出來的時(shí)候,可以用相對(duì)路徑也可以用絕對(duì)路來來提取,當(dāng)我們用相對(duì)路徑的時(shí)候,路徑就是相對(duì)于當(dāng)前的class文件所在目錄的路徑,如果是用絕對(duì)路徑的時(shí)候,路徑就是從JAR內(nèi)部的根目錄開始算的。把圖片等一些資源打入JAR包有很多好處,一是可以實(shí)現(xiàn)資源的初步隱藏,二是可以利用JAR的特性對(duì)文件進(jìn)行一些壓縮,因?yàn)镴AR包就是一個(gè)壓縮包,只不過后綴名改了而已。
下面我們?cè)賮砜匆幌聀aintComponent方法,它是一個(gè)重寫的方法,它重寫了父類JPanel里面的paintComponent方法,一般來說,當(dāng)我們要繪制一些內(nèi)容的時(shí)候,都是采用重寫此方法的辦法,在以前AWT的編程中,對(duì)重量型組件進(jìn)行重寫,一般重寫的是paint方法,所以在用輕量級(jí)組件的時(shí)候,這一點(diǎn)要注意,最好不要再重寫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);
它的意思就是設(shè)置圖形上下文在繪制的時(shí)候,要注意哪些方面,我們可以利用這個(gè)方法給圖形上下文一些提示。在本文里面我們提示了兩點(diǎn),一點(diǎn)是圖片抗鋸齒打開,二是文本抗據(jù)齒我們也打開,除了這兩個(gè)之外還要很多提示我們可以設(shè)置的,在興趣的朋友可以查看java.awt.RenderingHints這個(gè)類。利用這個(gè)特性,我們可以使我們呈現(xiàn)的界面更加完美,不過完美是需要代價(jià)的,呈現(xiàn)的越清晰越完美就越需要更多的CPU的運(yùn)算,所以當(dāng)電腦的性能不太好的時(shí)候,我們可以把這兩個(gè)提示去掉,讓JVM自行把握繪制的質(zhì)量。
還有一點(diǎn)我們要注意的地方,那就是我們調(diào)用repaint的地方。在我們需要重繪的時(shí)候,我們可以調(diào)用repaint方法,它會(huì)發(fā)送一個(gè)重繪的請(qǐng)求,那個(gè)會(huì)把這個(gè)請(qǐng)求放到重繪線程里面去,在我們調(diào)用repaint的時(shí)候,有很重要的一點(diǎn)就是盡量不要去調(diào)用repaint的默認(rèn)方法,而要調(diào)用repaint(int
x,int y,int width,int height)方法,因?yàn)樗粫?huì)請(qǐng)求重繪某一個(gè)區(qū)域,而repaint()則會(huì)重繪整個(gè)區(qū)域,所以為了性能著想,最好不要重繪整個(gè)區(qū)域,當(dāng)你開發(fā)了有關(guān)JAVA2D的程序后,你會(huì)發(fā)現(xiàn),程序的大部份CPU都耗在重繪上面,所以優(yōu)化重繪區(qū)域?qū)τ趦?yōu)化整個(gè)程序的性能是很有效果的。
盡管千里冰封
依然擁有晴空
你我共同品味JAVA的濃香.