老實說,延遲下載游戲資源及調用只是一種輔助手段,與游戲開發本身關系並不大,實質也無非只是文件下載及文件讀取的混用。但考慮到上周有網友問及此類問題,筆者覺得與其回郵件單獨解釋,倒不如寫篇博文看起來更具體清晰,還能令大家幫助筆者斧正刊誤,故成此文,僅供參考。
一般來講,我們之所以會需要通過下載方式加載游戲資源,無非是出於如下幾種目的:
1、精簡游戲體積:
假設我做了個100MB的游戲,卻非想把它宣傳成僅有1MB的精巧程序,這時我該怎麼辦呢?
去欺騙用戶,用大量復雜的技術名詞忽悠他們說1MB和100MB等值嗎?——用戶不是傻子,至少不都是傻子,無論你的話術多麼巧妙,也很難讓絕大部分人都相信1MB和100MB是一樣的。但大話已然出口,始終要想辦法解決。
其實呢,在現有技術體系下,要搞定他們好簡單的,只需將游戲初始界面混合下載器打包成1MB的文件發布,再“騙”他們下載執行,而後——就讓他們慢慢等待系統加載剩下那99MB吧!畢竟沒人說過這個游戲不需要額外的網絡資源同步嘛……
2、網游資源的延遲加載需要:
目前的網絡游戲——特別是網頁游戲,為了盡可能的減少不必要的資源損耗,提高運行效率,大多數時候並不會一口氣將所有資源都加載到游戲中,而是“大而化之,分而治之”,將游戲資源構建成一個個小小的資源包,僅僅在需要時,才或同步、或異步的加載到游戲中。這也正是我們在很多網游中所見到的,當角色過屏、讀取新地圖或遭遇新怪物時,畫面會出現稍候字樣或者部分馬賽克乃至停頓的原因。
故此,通過網絡適時地去加載需要的資源,幾乎已成為網游開發中必不可少的技巧之一。
3、融入特殊的加密解密機制:
我們都知道,但凡是人所做出的程序,就沒有人所不能破解掉的。但是——卻很可能發生一個人做出來的程序,另一個人數年之內無法破解的現象。而當數年之後,另一個人破解出來時,這段程序卻早已過氣,白送都沒人要了。
因此,當你極端的不想自己游戲被反向工程——尤其是想保護Java這種極好反編譯的代碼時,通過網絡下載的另一種意義便顯現出來了。你可以將下載的jar 或class乃至其它種種保存到一個不同於執行目錄的“隱秘”場所,並且無論密鑰也好,特殊結構也罷,總之變著方的將資源加密混淆,就算混淆到連你自己都不知道這是什麼東西也無所謂——能解釋成字節碼就好,最大限度的增加反向難度。而當你執行完畢,再一刪了事——下次還可以再下嘛。這樣做的話,雖然不能徹底杜絕代碼被他人盜用,但,至少也可讓反向我代碼那哥們累掉層皮(^^)。
4、本地程序及資源合法性驗證:
在大多數網絡游戲中,為了保證用戶不做出一些諸如使用外掛的“犯規”行為,是會對系統環境乃至封包數據進行合法性驗證的,而一旦發現“非法”的東西存在,則會令“違法”玩家吊線或者干脆封號以示懲罰。
但這些驗證,主要都只針對程序“外部”,即當“犯規”對象“不是我的游戲時”才能發揮功效,但萬一“犯規”者“來自游戲本身時”或者“驗證程序認為來自游戲本身時”,則變得無能為力,這也是為什麼大多數網游都“內掛”泛濫的緣故。
幸運的是,Java程序由於其“天資所限”,是很難在虛擬機外部被攻破利用的,如果用Java制作網游,原則上大可不必擔心“內掛”問題——但,這也有個大前提,那就是在“內掛”運行於虛擬機之外時才行的通。
而如果“內掛”運行在虛擬機之內呢? 如果我的“內掛”是一小段插入原始游戲中的代碼呢? 要知道,動態加載class,動態修改字節碼,早就不算什麼事情了。
這時,就需要校驗Java程序的合法性。
本來要驗證這種事情是比較耗費時間的,但如果我們善於利用每次下載資源(比較大的,比如過圖或者游戲更新),如果不單單“下載”,更同步“上行”,利用空檔同服務器校對本地Java程序的合法性及完整性,便能很大程度上避免這種無意義的校驗時間浪費。這時有缺少的文件便添加,有多余的——也就是出現不該存在的Jar或class乃至原始字節碼修改,便借機強行“咔”掉它,免得它“為禍人間”。
5、系統升級:
通常來說,大多數的程序是不可能一個版本用到底的,游戲也不例外,無論是功能的增加或者Bug修正都離不開系統的更新。為了避免每次升級都令用戶面臨重裝的苦惱,通過網絡下載更新資源並自動更新系統也就變得非常必要。
如何實現:
下面我給出一個簡單的資源延遲下載Java實現示例,分別加載我以前博文中出現的兩個示例程序,並運行其中之一。(PS:由於本例加載的資源為Jar,所以進行了動態類加載操作,當我們下載其它資源類型,比如僅包含圖片的zip壓縮文件時,下載的步驟還是一致,只不過要將操作換成讀取壓縮文件及加載壓縮文件資源而已。)
DownloadTool.java(這是一個簡單的下載工具類,內置有下載狀態顯示及下載條繪制)
package org.loon.game.simple.download;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.io.File;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
/**
* Copyright 2008 - 2009
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* [url]http://www.apache.org/licenses/LICENSE-2.0[/url]
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* @project loonframework
* @author chenpeng
* @email:[email protected]
* @version 0.1
*/
public class DownloadTool implements Runnable {
final static private Font font = new Font("黑體", 0, 14);
private Rectangle rectangle;
final static private Image barImage = GraphicsUtils
.loadImage("image/bar.png");
final static private int MAX_BUFFER_SIZE = 2048;
final static private int DOWNLOADING = 0;
final static private int PAUSED = 1;
final static private int COMPLETE = 2;
final static private int CANCELLED = 3;
final static private int ERROR = 4;
private Image backgroundBarImage;
private Image progressBarImage;
private Image dialogBarImage;
private URL url;
private int size;
private int downloaded;
private int contentLength;
private int status;
private DownloadListen listen;
private String downloadName;
/**
* 創建進度條提示框
*
* @param object
* @param w
* @param h
* @param filtrate
* @return
*/
private static Image createDialog(Image object, int w, int h,
boolean filtrate) {
Image barImage = null;
if (filtrate) {
barImage = GraphicsUtils.drawClipImage(object, 249, 30, 1, 57);
barImage = GraphicsUtils.transBlackColor(barImage);
} else {
barImage = GraphicsUtils.drawClipImage(object, 249, 27, 0, 0);
}
Image imageLeft = GraphicsUtils.drawClipImage(barImage, 8, 27, 0, 0);
Image imageRight = GraphicsUtils.drawClipImage(barImage, 8, 27, 241, 0);
Image ImageCenter = GraphicsUtils
.drawClipImage(barImage, 233, 27, 8, 0);
ImageCenter = GraphicsUtils.getResize(ImageCenter, w, h);
Graphics cg = ImageCenter.getGraphics();
cg.drawImage(imageLeft, 0, 0, null);
cg.drawImage(imageRight, w - 8, 0, null);
cg.dispose();
return ImageCenter;
}
/**
* 構造函數,加載指定url
*
* @param url
*/
public DownloadTool(String url) {
this.size = -1;
this.downloaded = 0;
this.status = DOWNLOADING;
try {
this.url = new URL(url);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
public DownloadTool(URL url) {
this.url = url;
this.size = -1;
this.downloaded = 0;
this.status = DOWNLOADING;
}
/**
* 設定進度條所在方位
*
* @param x
* @param y
* @param w
* @param h
*/
public void setRectangle(int x, int y, int w, int h) {
setRectangle(new Rectangle(x, y, w, h));
}
/**
* 設定進度條所在方位
*
* @param rectangle
*/
public void setRectangle(Rectangle rectangle) {
this.backgroundBarImage = GraphicsUtils.drawClipImage(barImage, 249,
27, 0, 0);
this.backgroundBarImage = createDialog(backgroundBarImage,
rectangle.width - 21, rectangle.height - 2, false);
// this.progressBarImage = GraphicsUtils.drawClipImage(barImage, 27, 27,
// 28, 28);
// this.progressBarImage = GraphicsUtils.drawClipImage(barImage, 27, 27,
// 56, 28);
this.progressBarImage = GraphicsUtils.drawClipImage(barImage, 27, 27,
0, 28);
this.dialogBarImage = createDialog(barImage, rectangle.width,
rectangle.height, true);
this.rectangle = rectangle;
}
/**
* 返回當前url地址
*
* @return
*/
public String getUrl() {
return url.toString();
}
/**
* 返回當前下載文件總長度
*
* @return
*/
public int getSize() {
return size;
}
/**
* 返回當前下載已完成長度
*
* @return
*/
public int getLevel() {
return downloaded;
}
/**
* 返回當前進度
*
* @return
*/
public int getProgress() {
return (int) ((double) downloaded / (double) size * 100);
}
public int getStatus() {
return status;
}
public void pause() {
status = PAUSED;
}
public void resume() {
status = DOWNLOADING;
download(listen);
}
public void cancel() {
status = CANCELLED;
}
private void error() {
status = ERROR;
}
public int getContentLength() {
return contentLength;
}
public void download(DownloadListen listen) {
this.listen = listen;
Thread thread = new Thread(this);
thread.start();
}
private String getFileName(URL url) {
String fileName = url.getFile();
return fileName.substring(fileName.lastIndexOf('/') + 1);
}
public String getFileName() {
return getFileName(url);
}
public boolean isExists() {
return new File(getFileName()).exists();
}
public String getDownloadName() {
return downloadName;
}
public void setDownloadName(String downloadName) {
this.downloadName = downloadName;
}
/**
* 進行文件下載並顯示進度
*/
public void run() {
if (!isExists()) {
RandomAccessFile file = null;
InputStream stream = null;
try {
HttpURLConnection connection = (HttpURLConnection) url
.openConnection();
connection.setRequestProperty("Range", "bytes=" + downloaded
+ "-");
connection.connect();
if (connection.getResponseCode() / 100 != 2) {
error();
}
contentLength = connection.getContentLength();
if (contentLength < 1) {
error();
}
if (size == -1) {
size = contentLength;
}
file = new RandomAccessFile(getFileName(url), "rw");
file.seek(downloaded);
stream = connection.getInputStream();
while (status == DOWNLOADING) {
byte buffer[];
if (size - downloaded > MAX_BUFFER_SIZE) {
buffer = new byte[MAX_BUFFER_SIZE];
} else {
buffer = new byte[size - downloaded];
}
int read = stream.read(buffer);
if (read == -1) {
break;
}
file.write(buffer, 0, read);
downloaded += read;
listen.updateScreen();
}
if (status == DOWNLOADING) {
status = COMPLETE;
listen.call();
}
} catch (Exception e) {
error();
} finally {
if (file != null) {
try {
file.close();
file = null;
} catch (Exception e) {
}
}
if (stream != null) {
try {
stream.close();
stream = null;
} catch (Exception e) {
}
}
}
} else {
status = COMPLETE;
listen.call();
}
}
/**
* 繪制下載進度
*
* @param g
*/
public synchronized void draw(Graphics g) {
int progress = getProgress();
double ratio = (double) ((double) getLevel() / (double) getSize());
int offset = (int) (rectangle.width * ratio);
g.drawImage(backgroundBarImage, rectangle.x + 19, rectangle.y, null);
g.drawImage(progressBarImage, rectangle.x + 1, rectangle.y,
offset + 20, rectangle.height - 2, null);
g.drawImage(dialogBarImage, rectangle.x, rectangle.y, null);
g.setFont(font);
String mes = (getDownloadName() + ",已完成進度 : " + progress + " %")
.intern();
FontMetrics fm = g.getFontMetrics();
int w = fm.stringWidth(mes);
int h = fm.getHeight() + 2;
g.setColor(Color.white);
GraphicsUtils.setRenderingHints(g);
g.drawString(mes, (rectangle.x + rectangle.width) / 2 - w / 2,
rectangle.y + h);
}
}
DownloadCanvas.java(下載條及背景顯示用畫布)
package org.loon.game.simple.download;
import java.awt.Canvas;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.image.BufferStrategy;
import java.util.ArrayList;
import java.util.List;
/**
* Copyright 2008 - 2009
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* [url]http://www.apache.org/licenses/LICENSE-2.0[/url]
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* @project loonframework
* @author chenpeng
* @email:[email protected]
* @version 0.1
*/
public class DownloadCanvas extends Canvas implements DownloadListen {
/**
*
*/
private static final long serialVersionUID = 1L;
private DownloadTool progress;
private Graphics canvasGraphics = null;
private BufferStrategy bufferStrategy;
private Image backgroundImage = GraphicsUtils
.loadImage("image/background.jpg");
private boolean initFlag;
private Window window;
final static List downloadList = new ArrayList(2);
/**
* 預定下載的文件
*/
static {
DownloadTool download1 = new DownloadTool(
"http://loon-simple.googlecode.com/files/Java25DSimple.jar");
download1.setDownloadName("下載Java2.5D八法行走示例中");
DownloadTool download2 = new DownloadTool(
"http://greenvm.googlecode.com/files/LocalOS_src.rar");
download2.setDownloadName("下載Java外掛入門示例中");
downloadList.add(download1);
downloadList.add(download2);
}
public void call() {
if (downloadList.size() == 0) {
window.setVisible(false);
window.dispose();
JarLoaderUtils.callJarMain("Java25DSimple.jar");
} else {
progress = (DownloadTool) downloadList.remove(0);
progress.setRectangle(rectangle);
progress.download(this);
}
}
final Rectangle rectangle;
public void createBufferGraphics() {
createBufferStrategy(2);
bufferStrategy = getBufferStrategy();
}
public DownloadCanvas(Window window, int width, int height) {
int pw = 600;
int ph = 27;
this.rectangle = new Rectangle(width / 2 - pw / 2, height / 2 - ph / 2,
pw, ph);
this.window = window;
this.call();
}
public synchronized void updateScreen() {
canvasGraphics = bufferStrategy.getDrawGraphics();
if (!initFlag) {
canvasGraphics.drawImage(backgroundImage, 0, 0, null);
initFlag = true;
} else {
progress.draw(canvasGraphics);
}
bufferStrategy.show();
canvasGraphics.dispose();
Toolkit.getDefaultToolkit().sync();
Thread.yield();
}
}
Main.java(主類,用以啟動此下載示例)
package org.loon.game.simple.download;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
* Copyright 2008 - 2009
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* [url]http://www.apache.org/licenses/LICENSE-2.0[/url]
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* @project loonframework
* @author chenpeng
* @email:[email protected]
* @version 0.1
*/
public class Main extends Frame {
/**
*
*/
private static final long serialVersionUID = 1L;
public Main(String titleName, int width, int height) {
this.setTitle(titleName);
this.setBackground(Color.black);
this.setPreferredSize(new Dimension(width + 5, height + 25));
this.requestFocus();
this.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
DownloadCanvas progress = new DownloadCanvas(this,width,height);
this.add(progress);
this.pack();
progress.createBufferGraphics();
this.setResizable(false);
this.setLocationRelativeTo(null);
this.setIgnoreRepaint(true);
this.setVisible(true);
}
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new Main("下載游戲數據", 640, 480);
}
});
}
}
程序執行效果如下圖所示:
下載完畢後後將自動加載並執行下載的Java2.5D行走示例,畫面如下圖:
(源碼依舊在Jar中)
源碼下載地址:http://code.google.com/p/loon-simple/downloads/list
本文出自 “Java究竟怎麼玩” 博客,請務必保留此出處http://cping1982.blog.51cto.com/601635/154277