2.5D遊戲,雖然在外觀上近似於3D遊戲,卻又不是嚴格意義上講的3D遊戲,故此2.5D遊戲又常被稱爲[僞3D遊戲]。
在筆者的觀念中,2.5D嚴格上說並不能算是一種技術,而只是一種實現方式或者說應用手段。大多數時候,遊戲公司之所以會採取2.5D方式開發遊戲,常是爲解決3D及2D技術混用而採取的一種折中,而並不是說這種手段有多麼先進。2.5D遊戲的實現方式雖然很多,但主要無非有三類,即:2D角色+3D場景(比如RO1)、3D角色+2D場景(比如生化復刻版)、2D角色+2D場景(比如仙劍),另外有些純3D遊戲出於操作性考慮而固定視角,勉強擦了個2.5D的邊,但嚴格上講依舊是3D。
除了2D角色+2D場景是利用斜45度的2D圖片來“冒充”3D效果以外,其它組合方式大多有真正的3D演算參與其中,不過是出於開發效率或者遊戲風格等外部因素考慮而混入2D,可以說是一種產生在2D向3D演變過程中的過渡物。
使用2.5D方式開發遊戲的好處在於,能夠免去純3D圖像渲染所涉及的海量運算,從而減低不必要的系統資源損耗,並且在處理單純的2D畫面時,貼圖也明顯較系統渲染爲快,最主要的是——開發週期明顯較3D遊戲更短。
當然,缺點也很明顯,最核心的一點就夠——不倫不類,非驢非馬。
具體到2.5D遊戲開發,根據選擇的技術不同,也會產生不同的實現手段,筆者在[Java中2.5D遊戲(斜45度角)的設計與實現]系列博文中內所要介紹的,是最簡單,同時也最方便的2D角色+2D場景——僞45度角方式,不需要的可以無視此文了,具體到真3D與2D的混合,筆者將留到介紹JME時再去講解。
在正式開始本回之前,我們先來簡要介紹下不同維度空間的特點:
零維,簡單講就是沒有維度,表現形式上就是一個[點],沒有長度,更沒有高度。實際上,很多人認爲宇宙最初就是個零維空間。
一維,多體現爲一條[直線],也可以理解爲一個只有長度,而沒有高度的無限線性空間。
二維,即我們常說的[平面],由X及Y兩點交織而成,即只有長與寬,典型的二維空間存在方式是數據表格。
三維,三維即[立體],若誰不能理解三維,請隨意向顯示器四周看看,舉目所及全是三維空間。從技術角度解釋,三維就是在二維的長、寬基礎上再加個高度(厚度)形成的體積面,典型的三維存在方式就是我們這個世界。
四維,四維空間是個非常模糊的概念,實際上它象徵着[n維]。在物理學及數學概念中,一個n的序列可以被理解爲一個n維空間中的位置,當n=4時,所有這樣的位置的集合都叫做四維空間,但是當n-1或者n+1時,它又會自然過渡成其它空間。這種空間與我們熟悉並在其中居住的三維空間不同,因爲它多一個維度。這個額外的維度既可以理解成時間,也可以直接理解爲空間的第四維,即第四空間維度。當我們說到四維空間時,常會扯出天堂、地獄、陰陽界等超自然理論,實在玄之又玄,筆者也不知道它究竟怎樣表示纔好……
由於人類生存在三維空間,我們周圍的空間自然也都具有三個維度(上下(長)、左右(寬)、前後(厚度)),用中式思維解釋就是世有八荒(又稱八方,即“東、西、南、北、東南、東北、西南、西北”八個方向),人居六合(即“上、下、東、西、南、北”六個方位),我們也都很自然的會用“八荒六合”來進行方位判定,當然,最先決的方位判定條件是“我在其中”。
如果一個人能在“八荒六合”之內與我一樣行動,我當然會認爲他與我一樣練成了八荒六合唯我獨尊功……咳,我是說認爲他與我同樣是一個立體的人,而不是一張紙片,嗯嗯(=_=|||)。
如果一個人能在“八荒六合”之內與我一樣行動,我當然會認爲他與我一樣練成了八荒六合唯我獨尊功……咳,我是說認爲他與我同樣是一個立體的人,而不是一張紙片,嗯嗯(=_=|||)。
同樣的道理,在遊戲中如果一個2D Sprite的行動模式與3D Sprite一致,那麼用戶便很容易“誤認”此單元爲3D,而非2D。原因就在於,人眼是極好矇蔽的,比如好萊塢早期大片中就經常使用紙製建築來冒充城鎮或者某個名聲古蹟;即便遊戲2.5D遊戲中沒有實際的3D座標及多面計算,只要能給人眼以“距離感”或者說“立體感”,我們也會認爲這個遊戲是三維存在的,而沒人會去關心它是否真正使用了3D渲染方式。
換句話說,只要我們創建的遊戲單元“看上去”能“行八荒”,“遊六合”,也就是看上去它的行爲是3D立體的,玩家就會認爲角色正處於一個三維空間之內,而不是穿越到某個2D世界。
換句話說,只要我們創建的遊戲單元“看上去”能“行八荒”,“遊六合”,也就是看上去它的行爲是3D立體的,玩家就會認爲角色正處於一個三維空間之內,而不是穿越到某個2D世界。
那麼,這個“看上去移動”的效果要如何達到呢?
我們用下圖作爲示例:
通過上圖中我們可以發現,此圖中角色的單元動作被分解爲八種不同類型,即上、左、右、下、左上、右上、左下、右下,而此八種動作正好對應着“八荒”中的“北、西、東、南、西北、東北、西南、東南”。由於人眼所能觀測到的運行是相對的,只要背景或者角色中任意一者發生移動,我們都會產生“移動”的“錯覺”。所以我們並不追求角色的實際3D運動,而只是令角色單元能做出對應“上、左、右、下、左上、右上、左下、右下”這八方向的動作,在普通人眼中,便與角色移動向“北、西、東、南、西北、東北、西南、東南”這八個方向無異。
因此,在2D製作2.5D效果時,每個角色的動作單元實際上都只是一幅分幀小圖罷了。
如何判定對應的單元圖像?
具體到單元動作的顯示判定,和遊戲採取的斜視角產生方式息息相關,大體上分可分兩類,即非等距座標實現與等距座標實現。
1、非等距座標實現:
1、非等距座標實現:
邏輯如下圖:
以0點爲常模(參照物),在2D地圖上“錯位”繪製角色,每次角色移動時偏移相應座標,配合圖片產生45度移動錯覺。
優點:在編程上極容易實現,並且更利於地圖及角色的聯合操作。
缺點:如果不對單元與地圖的交織部分進行細節處理,很容易產生移動“生硬”感,並且很難融入一些較複雜的地形判定。
2、等距座標實現:
邏輯如下圖:
實際上此圖沒有任何座標變化,而是在不改變2D圖形X,Y座標系的基礎上,等距轉換座標點位置爲斜45度時的狀態,由於座標系經過等距轉換,所以實際操作中與2D無疑。
優點:除了座標轉換外,基本上還是2D那一套,純2D時怎樣處理便怎樣處理。
缺點:爲了配合傾斜後的X,Y座標拼接,大部分平面圖必須轉換爲45度圖,當然這是美工的事(^^)(PS:雖然也可以在平面圖上自動換算出所需的斜視圖形,但細節處通常不夠理想,而且耗費不必要的運算資源,還是交給美工直接切出成品圖最好,鄙人大原則就是能麻煩美工就不勞駕程序員(^^)),不過象筆者這樣個人研究就超麻煩……
在本系列博文在後面會涉及到此部分,在這裏先給出一個基本概念。
首先,要轉換座標爲斜45度時位置,至少需要以下參數。
1、mapX(地圖X座標)
2、mapY(地圖Y座標)
3、mapMaxY (Y軸的最大縱深)
4、tileWidth(每塊小圖寬度)
5、tileHeight(每塊小圖高度)
2、mapY(地圖Y座標)
3、mapMaxY (Y軸的最大縱深)
4、tileWidth(每塊小圖寬度)
5、tileHeight(每塊小圖高度)
而後我們纔可以根據基礎參數換算座標位置:
screenX (屏幕座標X)
screenY (屏幕座標Y)
screenY (屏幕座標Y)
screenX = (mapX - mapY + mapMaxY) * (tileWidth / 2);
screenY = (mapX + mapY) * (tileHeight / 2);
bevelMapX (傾視的X座標)
bevelMapY (傾視的Y座標)
screenY = (mapX + mapY) * (tileHeight / 2);
bevelMapX (傾視的X座標)
bevelMapY (傾視的Y座標)
bevelMapX = ((screenY / tileHeight) + (screenX - (mapMaxY * tileWidth/2)) / tileWidth);
bevelMapY = ((screenY / tileHeight) - (screenX - (mapMaxY * tileWidth/2)) / tileWidth);
bevelMapY = ((screenY / tileHeight) - (screenX - (mapMaxY * tileWidth/2)) / tileWidth);
這時得到的bevelMapX及bevelMapY,就是斜45度時的繪圖位置,以此座標繪製準備好的斜視圖,就自然會呈現在斜視情況下的X,Y點位置上。
由於本例中爲非等距實現,也就是在2D地圖上直接位移角色單元到斜點,故此不需要額外的進行地圖座標換算處理,但是人物座標則需偏移。
下面開始我們用代碼示例說話:
Role.java(負責描述一個角色單元的行爲)
package org.loon.game.simple.alldirection.rpg;
import java.awt.Color;
import java.awt.Graphics;
import java.util.List;
import org.loon.game.simple.alldirection.GraphicsUtils;
public class Role implements Config {
private static final int SPEED = 4;
public static final double PROB_MOVE = 0.02;
private int x, y;
private int px, py;
private int direction;
private int count;
private boolean isMoving;
private int movingLength;
private int moveType;
private String message;
private Thread threadAnime;
private RpgSprite sprite;
private RpgMap map;
private String name;
private String partyName;
private int ioffsetX;
private int ioffsetY;
private boolean autoFinder;
private boolean isLoop;
public Role(String fileName, int x, int y, int direction, int moveType,
RpgMap map) {
sprite = new RpgSprite(fileName);
this.x = x;
this.y = y;
px = x * CS;
py = y * CS;
ioffsetX = sprite.getImageWidth() - CS;
ioffsetY = sprite.getImageHeight() - CS;
this.direction = direction;
this.count = 0;
this.moveType = moveType;
this.map = map;
this.roleLoop();
}
private void roleLoop() {
isLoop = true;
threadAnime = new Thread(new AnimationThread());
threadAnime.start();
}
public void stop() {
isLoop = false;
threadAnime = null;
}
public void setXandY(Cell2D cell) {
setXandY(cell.x(), cell.y());
}
public void setXandY(int x, int y) {
this.x = x;
this.y = y;
}
public Cell2D getCell2D() {
return new Cell2D(x, y);
}
private synchronized void redress() {
if (autoFinder) {
move();
}
if (px < 0) {
px = 0;
}
if (py < 0) {
py = 0;
}
if (px > map.getWidth() - CS) {
px = map.getWidth() - CS;
x = map.getRow() - 1;
}
if (py > map.getHeight() - CS) {
py = map.getHeight() - CS;
y = map.getCol() - 1;
}
}
public synchronized void draw(Graphics g, int offsetX, int offsetY) {
redress();
int aspect = 0;
switch (direction) {
case UP:
aspect = RpgSprite.UPPER_RIGHT;
break;
case DOWN:
aspect = RpgSprite.LOWER_LEFT;
break;
case LEFT:
aspect = RpgSprite.UPPER_LEFT;
break;
case RIGHT:
aspect = RpgSprite.LOWER_RIGHT;
break;
case TUP:
aspect = RpgSprite.UP;
break;
case TDOWN:
aspect = RpgSprite.DOWN;
break;
case TLEFT:
aspect = RpgSprite.LEFT;
break;
case TRIGHT:
aspect = RpgSprite.RIGHT;
break;
}
int nx = px + offsetX - ioffsetX;
int ny = py + offsetY - ioffsetY;
g.drawImage(sprite.getMove(aspect)[count], nx, ny, null);
if (name != null) {
int fontHeight = g.getFontMetrics().getHeight();
int nameFontWidth = g.getFontMetrics().stringWidth(name);
int size = 20;
int mx = nx + ioffsetX - nameFontWidth / 2;
int my = ny + ioffsetY + size + fontHeight;
GraphicsUtils.drawStyleString(g, name, mx, my, Color.black,
Color.white);
GraphicsUtils.drawStyleString(g, partyName, mx, my + size,
Color.black, Color.white);
}
}
public synchronized void autoDirection(List startPath) {
Cell2D cell1 = (Cell2D) startPath.get(0);
try {
if (startPath.size() > 1) {
Cell2D cell2 = (Cell2D) startPath.get(1);
int sx = cell2.x() - cell1.x();
int sy = cell2.y() - cell1.y();
direction = Field2D.getDirection(sx, sy);
}
} finally {
startPath.remove(0);
}
}
public synchronized boolean move() {
switch (direction) {
case LEFT:
if (moveLowerLeft()) {
return true;
}
break;
case RIGHT:
if (moveLowerRight()) {
return true;
}
break;
case UP:
if (moveUpperRight()) {
return true;
}
break;
case DOWN:
if (moveUpperLeft()) {
return true;
}
break;
case TLEFT:
if (moveLeft()) {
return true;
}
break;
case TRIGHT:
if (moveRight()) {
return true;
}
break;
case TUP:
if (moveUp()) {
return true;
}
break;
case TDOWN:
if (moveDown()) {
return true;
}
break;
}
return false;
}
protected boolean moveLeft() {
int nextX = x - 1;
int nextY = y;
if (nextX < 0) {
nextX = 0;
}
if (!map.isHit(nextX, nextY)) {
px -= Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x--;
px = x * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveRight() {
int nextX = x + 1;
int nextY = y;
if (nextX > map.getCol() - 1) {
nextX = map.getCol() - 1;
}
if (!map.isHit(nextX, nextY)) {
px += Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x++;
px = x * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveUp() {
int nextX = x;
int nextY = y - 1;
if (nextY < 0) {
nextY = 0;
}
if (!map.isHit(nextX, nextY)) {
py -= Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
y--;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveDown() {
int nextX = x;
int nextY = y + 1;
if (!map.isHit(nextX, nextY)) {
py += Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
y++;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveLowerLeft() {
int nextX = x - 1;
int nextY = y - 1;
if (nextX < 0) {
nextX = 0;
}
if (nextY < 0) {
nextY = 0;
}
if (!map.isHit(nextX, nextY)) {
px -= Role.SPEED;
py -= Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x--;
px = x * CS;
y--;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveLowerRight() {
int nextX = x + 1;
int nextY = y + 1;
if (nextX > map.getRow() - 1) {
nextX = map.getRow() - 1;
}
if (nextY > map.getCol() - 1) {
nextY = map.getCol() - 1;
}
if (!map.isHit(nextX, nextY)) {
px += Role.SPEED;
py += Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x++;
px = x * CS;
y++;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveUpperLeft() {
int nextX = x - 1;
int nextY = y + 1;
if (nextX < 0) {
nextX = 0;
}
if (nextY > map.getCol() - 1) {
nextY = map.getCol() - 1;
}
if (!map.isHit(nextX, nextY)) {
px -= Role.SPEED;
py += Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x--;
px = x * CS;
y++;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveUpperRight() {
int nextX = x + 1;
int nextY = y - 1;
if (nextX > map.getRow() - 1) {
nextX = map.getRow() - 1;
}
if (nextY < 0) {
nextY = 0;
}
if (!map.isHit(nextX, nextY)) {
px += Role.SPEED;
py -= Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x++;
px = x * CS;
y--;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
public Role talkWith() {
int nextX = 0;
int nextY = 0;
switch (direction) {
case LEFT:
nextX = x - 1;
nextY = y;
break;
case RIGHT:
nextX = x + 1;
nextY = y;
break;
case UP:
nextX = x;
nextY = y - 1;
break;
case DOWN:
nextX = x;
nextY = y + 1;
break;
}
Role chara;
chara = map.getRoles().roleCheck(nextX, nextY);
if (chara != null) {
switch (direction) {
case LEFT:
chara.setDirection(RIGHT);
break;
case RIGHT:
chara.setDirection(LEFT);
break;
case UP:
chara.setDirection(DOWN);
break;
case DOWN:
chara.setDirection(UP);
break;
}
}
return chara;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getPx() {
return px;
}
public int getPy() {
return py;
}
public void setDirection(int dir) {
direction = dir;
}
public synchronized boolean isMoving() {
return isMoving;
}
public synchronized void setMoving(boolean flag) {
isMoving = flag;
movingLength = 0;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getMoveType() {
return moveType;
}
private class AnimationThread extends Thread {
public void run() {
while (isLoop) {
if (count < sprite.getSize()) {
count++;
} else {
count = 0;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
}
}
}
}
public boolean isAutoFinder() {
return autoFinder;
}
public void setAutoFinder(boolean autoFinder) {
this.autoFinder = autoFinder;
}
public int getIoffsetX() {
return ioffsetX;
}
public int getIoffsetY() {
return ioffsetY;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPartyName() {
return partyName;
}
public void setPartyName(String partyName) {
this.partyName = partyName;
}
}
import java.awt.Color;
import java.awt.Graphics;
import java.util.List;
import org.loon.game.simple.alldirection.GraphicsUtils;
public class Role implements Config {
private static final int SPEED = 4;
public static final double PROB_MOVE = 0.02;
private int x, y;
private int px, py;
private int direction;
private int count;
private boolean isMoving;
private int movingLength;
private int moveType;
private String message;
private Thread threadAnime;
private RpgSprite sprite;
private RpgMap map;
private String name;
private String partyName;
private int ioffsetX;
private int ioffsetY;
private boolean autoFinder;
private boolean isLoop;
public Role(String fileName, int x, int y, int direction, int moveType,
RpgMap map) {
sprite = new RpgSprite(fileName);
this.x = x;
this.y = y;
px = x * CS;
py = y * CS;
ioffsetX = sprite.getImageWidth() - CS;
ioffsetY = sprite.getImageHeight() - CS;
this.direction = direction;
this.count = 0;
this.moveType = moveType;
this.map = map;
this.roleLoop();
}
private void roleLoop() {
isLoop = true;
threadAnime = new Thread(new AnimationThread());
threadAnime.start();
}
public void stop() {
isLoop = false;
threadAnime = null;
}
public void setXandY(Cell2D cell) {
setXandY(cell.x(), cell.y());
}
public void setXandY(int x, int y) {
this.x = x;
this.y = y;
}
public Cell2D getCell2D() {
return new Cell2D(x, y);
}
private synchronized void redress() {
if (autoFinder) {
move();
}
if (px < 0) {
px = 0;
}
if (py < 0) {
py = 0;
}
if (px > map.getWidth() - CS) {
px = map.getWidth() - CS;
x = map.getRow() - 1;
}
if (py > map.getHeight() - CS) {
py = map.getHeight() - CS;
y = map.getCol() - 1;
}
}
public synchronized void draw(Graphics g, int offsetX, int offsetY) {
redress();
int aspect = 0;
switch (direction) {
case UP:
aspect = RpgSprite.UPPER_RIGHT;
break;
case DOWN:
aspect = RpgSprite.LOWER_LEFT;
break;
case LEFT:
aspect = RpgSprite.UPPER_LEFT;
break;
case RIGHT:
aspect = RpgSprite.LOWER_RIGHT;
break;
case TUP:
aspect = RpgSprite.UP;
break;
case TDOWN:
aspect = RpgSprite.DOWN;
break;
case TLEFT:
aspect = RpgSprite.LEFT;
break;
case TRIGHT:
aspect = RpgSprite.RIGHT;
break;
}
int nx = px + offsetX - ioffsetX;
int ny = py + offsetY - ioffsetY;
g.drawImage(sprite.getMove(aspect)[count], nx, ny, null);
if (name != null) {
int fontHeight = g.getFontMetrics().getHeight();
int nameFontWidth = g.getFontMetrics().stringWidth(name);
int size = 20;
int mx = nx + ioffsetX - nameFontWidth / 2;
int my = ny + ioffsetY + size + fontHeight;
GraphicsUtils.drawStyleString(g, name, mx, my, Color.black,
Color.white);
GraphicsUtils.drawStyleString(g, partyName, mx, my + size,
Color.black, Color.white);
}
}
public synchronized void autoDirection(List startPath) {
Cell2D cell1 = (Cell2D) startPath.get(0);
try {
if (startPath.size() > 1) {
Cell2D cell2 = (Cell2D) startPath.get(1);
int sx = cell2.x() - cell1.x();
int sy = cell2.y() - cell1.y();
direction = Field2D.getDirection(sx, sy);
}
} finally {
startPath.remove(0);
}
}
public synchronized boolean move() {
switch (direction) {
case LEFT:
if (moveLowerLeft()) {
return true;
}
break;
case RIGHT:
if (moveLowerRight()) {
return true;
}
break;
case UP:
if (moveUpperRight()) {
return true;
}
break;
case DOWN:
if (moveUpperLeft()) {
return true;
}
break;
case TLEFT:
if (moveLeft()) {
return true;
}
break;
case TRIGHT:
if (moveRight()) {
return true;
}
break;
case TUP:
if (moveUp()) {
return true;
}
break;
case TDOWN:
if (moveDown()) {
return true;
}
break;
}
return false;
}
protected boolean moveLeft() {
int nextX = x - 1;
int nextY = y;
if (nextX < 0) {
nextX = 0;
}
if (!map.isHit(nextX, nextY)) {
px -= Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x--;
px = x * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveRight() {
int nextX = x + 1;
int nextY = y;
if (nextX > map.getCol() - 1) {
nextX = map.getCol() - 1;
}
if (!map.isHit(nextX, nextY)) {
px += Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x++;
px = x * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveUp() {
int nextX = x;
int nextY = y - 1;
if (nextY < 0) {
nextY = 0;
}
if (!map.isHit(nextX, nextY)) {
py -= Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
y--;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveDown() {
int nextX = x;
int nextY = y + 1;
if (!map.isHit(nextX, nextY)) {
py += Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
y++;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveLowerLeft() {
int nextX = x - 1;
int nextY = y - 1;
if (nextX < 0) {
nextX = 0;
}
if (nextY < 0) {
nextY = 0;
}
if (!map.isHit(nextX, nextY)) {
px -= Role.SPEED;
py -= Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x--;
px = x * CS;
y--;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveLowerRight() {
int nextX = x + 1;
int nextY = y + 1;
if (nextX > map.getRow() - 1) {
nextX = map.getRow() - 1;
}
if (nextY > map.getCol() - 1) {
nextY = map.getCol() - 1;
}
if (!map.isHit(nextX, nextY)) {
px += Role.SPEED;
py += Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x++;
px = x * CS;
y++;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveUpperLeft() {
int nextX = x - 1;
int nextY = y + 1;
if (nextX < 0) {
nextX = 0;
}
if (nextY > map.getCol() - 1) {
nextY = map.getCol() - 1;
}
if (!map.isHit(nextX, nextY)) {
px -= Role.SPEED;
py += Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x--;
px = x * CS;
y++;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
protected boolean moveUpperRight() {
int nextX = x + 1;
int nextY = y - 1;
if (nextX > map.getRow() - 1) {
nextX = map.getRow() - 1;
}
if (nextY < 0) {
nextY = 0;
}
if (!map.isHit(nextX, nextY)) {
px += Role.SPEED;
py -= Role.SPEED;
movingLength += Role.SPEED;
if (movingLength >= CS) {
x++;
px = x * CS;
y--;
py = y * CS;
isMoving = false;
return true;
}
} else {
isMoving = false;
px = x * CS;
py = y * CS;
}
return false;
}
public Role talkWith() {
int nextX = 0;
int nextY = 0;
switch (direction) {
case LEFT:
nextX = x - 1;
nextY = y;
break;
case RIGHT:
nextX = x + 1;
nextY = y;
break;
case UP:
nextX = x;
nextY = y - 1;
break;
case DOWN:
nextX = x;
nextY = y + 1;
break;
}
Role chara;
chara = map.getRoles().roleCheck(nextX, nextY);
if (chara != null) {
switch (direction) {
case LEFT:
chara.setDirection(RIGHT);
break;
case RIGHT:
chara.setDirection(LEFT);
break;
case UP:
chara.setDirection(DOWN);
break;
case DOWN:
chara.setDirection(UP);
break;
}
}
return chara;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getPx() {
return px;
}
public int getPy() {
return py;
}
public void setDirection(int dir) {
direction = dir;
}
public synchronized boolean isMoving() {
return isMoving;
}
public synchronized void setMoving(boolean flag) {
isMoving = flag;
movingLength = 0;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getMoveType() {
return moveType;
}
private class AnimationThread extends Thread {
public void run() {
while (isLoop) {
if (count < sprite.getSize()) {
count++;
} else {
count = 0;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
}
}
}
}
public boolean isAutoFinder() {
return autoFinder;
}
public void setAutoFinder(boolean autoFinder) {
this.autoFinder = autoFinder;
}
public int getIoffsetX() {
return ioffsetX;
}
public int getIoffsetY() {
return ioffsetY;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPartyName() {
return partyName;
}
public void setPartyName(String partyName) {
this.partyName = partyName;
}
}
RprMap.java(負責描述角色單元集合在地圖上的具體位置)
package org.loon.game.simple.alldirection.rpg;
import java.awt.Color;
import java.awt.Graphics;
import java.io.IOException;
import java.util.List;
import org.loon.game.simple.alldirection.GraphicsUtils;
import org.loon.game.simple.alldirection.LSystem;
public class RpgMap implements Config {
private ImageMapFactory imageMap;
private Roles roles;
private boolean showGrid;
private Field2D map2d;
private int firstTileX;
private int firstTileY;
private int lastTileX;
private int lastTileY;
public RpgMap(String imageFile, String mapFile) {
try {
imageMap = new ImageMapFactory(imageFile, mapFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
this.map2d = new Field2D(imageMap.getMap());
this.roles = new Roles();
}
public void addRole(Role role) {
roles.addChara(role);
}
public Role getHero() {
return roles.getHero();
}
public void setupHero(Role hero) {
roles.mainHero(hero);
}
public synchronized void draw(Graphics g, int offsetX, int offsetY) {
firstTileX = pixelsToTiles(-offsetX);
lastTileX = firstTileX + pixelsToTiles(LSystem.WIDTH) + 1;
lastTileX = Math.min(lastTileX, getRow());
firstTileY = pixelsToTiles(-offsetY);
lastTileY = firstTileY + pixelsToTiles(LSystem.HEIGHT) + 1;
lastTileY = Math.min(lastTileY, getCol());
for (int i = firstTileX; i < lastTileX; i++) {
for (int j = firstTileY; j < lastTileY; j++) {
g.drawImage(imageMap.getImages()[i][j], tilesToPixels(i)
+ offsetX, tilesToPixels(j) + offsetY, null);
if (showGrid) {
if (imageMap.getMap()[j][i] == 1) {
g.setColor(Color.white);
g.drawRect(tilesToPixels(i) + offsetX, tilesToPixels(j)
+ offsetY, CS - 2, CS - 2);
GraphicsUtils.setAlpha(g, 0.5d);
g.fillRect(tilesToPixels(i) + offsetX, tilesToPixels(j)
+ offsetY, CS - 2, CS - 2);
GraphicsUtils.setAlpha(g, 1.0d);
} else if (imageMap.getMap()[j][i] == -1) {
g.setColor(Color.blue);
g.drawRect(tilesToPixels(i) + offsetX, tilesToPixels(j)
+ offsetY, CS - 2, CS - 2);
GraphicsUtils.setAlpha(g, 0.3d);
g.fillRect(tilesToPixels(i) + offsetX, tilesToPixels(j)
+ offsetY, CS - 2, CS - 2);
GraphicsUtils.setAlpha(g, 1.0d);
}
}
}
}
roles.draw(g, offsetX, offsetY);
}
public int getSelfFirstX() {
return firstTileX;
}
public int getSelfFirstY() {
return firstTileY;
}
public int getSelfLastX() {
return lastTileX;
}
public int getSelfLastY() {
return lastTileY;
}
public int getSelfFirstWidth() {
return tilesToPixels(firstTileX);
}
public int getSelfFirstHeight() {
return tilesToPixels(firstTileY);
}
public int getSelfLastWidth() {
return tilesToPixels(lastTileX);
}
public int getSelfLastHeight() {
return tilesToPixels(lastTileY);
}
public List findPath(Role hero, Cell2D goal) {
return AStarFinder.find(map2d, hero.getCell2D(), goal);
}
public void showGrid(boolean show) {
this.showGrid = show;
}
public ImageMapFactory getFactory() {
return imageMap;
}
public boolean isHit(int x, int y) {
try {
int[][] map = imageMap.getMap();
if (map[y][x] == 1) {
return true;
}
if (roles.isHit(x, y)) {
return true;
}
return false;
} catch (Exception e) {
return false;
}
}
public static int pixelsToTiles(double pixels) {
return (int) Math.floor(pixels / CS);
}
public static int tilesToPixels(int tiles) {
return tiles * CS;
}
public int getRow() {
return imageMap.getMapWidth();
}
public int getCol() {
return imageMap.getMapHeight();
}
public int getWidth() {
return imageMap.getImageWidth();
}
public int getHeight() {
return imageMap.getImageHeight();
}
public Roles getRoles() {
return roles;
}
}
import java.awt.Color;
import java.awt.Graphics;
import java.io.IOException;
import java.util.List;
import org.loon.game.simple.alldirection.GraphicsUtils;
import org.loon.game.simple.alldirection.LSystem;
public class RpgMap implements Config {
private ImageMapFactory imageMap;
private Roles roles;
private boolean showGrid;
private Field2D map2d;
private int firstTileX;
private int firstTileY;
private int lastTileX;
private int lastTileY;
public RpgMap(String imageFile, String mapFile) {
try {
imageMap = new ImageMapFactory(imageFile, mapFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
this.map2d = new Field2D(imageMap.getMap());
this.roles = new Roles();
}
public void addRole(Role role) {
roles.addChara(role);
}
public Role getHero() {
return roles.getHero();
}
public void setupHero(Role hero) {
roles.mainHero(hero);
}
public synchronized void draw(Graphics g, int offsetX, int offsetY) {
firstTileX = pixelsToTiles(-offsetX);
lastTileX = firstTileX + pixelsToTiles(LSystem.WIDTH) + 1;
lastTileX = Math.min(lastTileX, getRow());
firstTileY = pixelsToTiles(-offsetY);
lastTileY = firstTileY + pixelsToTiles(LSystem.HEIGHT) + 1;
lastTileY = Math.min(lastTileY, getCol());
for (int i = firstTileX; i < lastTileX; i++) {
for (int j = firstTileY; j < lastTileY; j++) {
g.drawImage(imageMap.getImages()[i][j], tilesToPixels(i)
+ offsetX, tilesToPixels(j) + offsetY, null);
if (showGrid) {
if (imageMap.getMap()[j][i] == 1) {
g.setColor(Color.white);
g.drawRect(tilesToPixels(i) + offsetX, tilesToPixels(j)
+ offsetY, CS - 2, CS - 2);
GraphicsUtils.setAlpha(g, 0.5d);
g.fillRect(tilesToPixels(i) + offsetX, tilesToPixels(j)
+ offsetY, CS - 2, CS - 2);
GraphicsUtils.setAlpha(g, 1.0d);
} else if (imageMap.getMap()[j][i] == -1) {
g.setColor(Color.blue);
g.drawRect(tilesToPixels(i) + offsetX, tilesToPixels(j)
+ offsetY, CS - 2, CS - 2);
GraphicsUtils.setAlpha(g, 0.3d);
g.fillRect(tilesToPixels(i) + offsetX, tilesToPixels(j)
+ offsetY, CS - 2, CS - 2);
GraphicsUtils.setAlpha(g, 1.0d);
}
}
}
}
roles.draw(g, offsetX, offsetY);
}
public int getSelfFirstX() {
return firstTileX;
}
public int getSelfFirstY() {
return firstTileY;
}
public int getSelfLastX() {
return lastTileX;
}
public int getSelfLastY() {
return lastTileY;
}
public int getSelfFirstWidth() {
return tilesToPixels(firstTileX);
}
public int getSelfFirstHeight() {
return tilesToPixels(firstTileY);
}
public int getSelfLastWidth() {
return tilesToPixels(lastTileX);
}
public int getSelfLastHeight() {
return tilesToPixels(lastTileY);
}
public List findPath(Role hero, Cell2D goal) {
return AStarFinder.find(map2d, hero.getCell2D(), goal);
}
public void showGrid(boolean show) {
this.showGrid = show;
}
public ImageMapFactory getFactory() {
return imageMap;
}
public boolean isHit(int x, int y) {
try {
int[][] map = imageMap.getMap();
if (map[y][x] == 1) {
return true;
}
if (roles.isHit(x, y)) {
return true;
}
return false;
} catch (Exception e) {
return false;
}
}
public static int pixelsToTiles(double pixels) {
return (int) Math.floor(pixels / CS);
}
public static int tilesToPixels(int tiles) {
return tiles * CS;
}
public int getRow() {
return imageMap.getMapWidth();
}
public int getCol() {
return imageMap.getMapHeight();
}
public int getWidth() {
return imageMap.getImageWidth();
}
public int getHeight() {
return imageMap.getImageHeight();
}
public Roles getRoles() {
return roles;
}
}
Main.java(初始配置及程序運行)
package org.loon.game.simple.alldirection.main;
import org.loon.game.simple.alldirection.GameCursor;
import org.loon.game.simple.alldirection.GameFrame;
import org.loon.game.simple.alldirection.rpg.Config;
import org.loon.game.simple.alldirection.rpg.Role;
import org.loon.game.simple.alldirection.rpg.RpgLayout;
import org.loon.game.simple.alldirection.rpg.RpgMap;
/**
* 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][email protected][/email]
* @version 0.1
*/
public class Main {
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
GameFrame frame = new GameFrame(
"Java 2.5D遊戲開發中的八方走法實現<LoonFramework-Game>", 640, 480);
// 設定遊標
frame.setCursor(GameCursor.getCursor("image/cursor.png"));
// 遊戲地圖
RpgMap rpgMap = new RpgMap("image/map/maze.jpg",
"./image/map/maze.map");
// 顯示網格
rpgMap.showGrid(false);
// 創建主角
Role hero = new Role("image/role/gm.png", 7, 8, Config.DOWN, 0,
rpgMap);
hero.setName("媽媽說壞孩子長大以後就是GM");
hero.setPartyName("曾經的北S商人");
// 創建NPC1
Role npc1 = new Role("image/role/assassin.png", 3, 14,
Config.LEFT, 1, rpgMap);
npc1.setName("煉妖狐(爆刺、爆刺、一拍即死)");
npc1.setPartyName("此人已死,有事燒紙");
// //創建NPC2
Role npc2 = new Role("image/role/rogue.png", 15, 21, Config.LEFT,
1, rpgMap);
npc2.setName("貓貓(巴帽小偷)");
npc2.setPartyName("病貓不發威你還拿我當老虎了");
// 設定主角
rpgMap.setupHero(hero);
// 設定NPC
rpgMap.addRole(npc1);
rpgMap.addRole(npc2);
frame.getGame().setControl(new RpgLayout(rpgMap));
// 遊戲全屏
// frame.updateFullScreen();
frame.setFPS(true);
frame.mainLoop();
frame.showFrame();
}
});
}
}
import org.loon.game.simple.alldirection.GameCursor;
import org.loon.game.simple.alldirection.GameFrame;
import org.loon.game.simple.alldirection.rpg.Config;
import org.loon.game.simple.alldirection.rpg.Role;
import org.loon.game.simple.alldirection.rpg.RpgLayout;
import org.loon.game.simple.alldirection.rpg.RpgMap;
/**
* 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][email protected][/email]
* @version 0.1
*/
public class Main {
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
GameFrame frame = new GameFrame(
"Java 2.5D遊戲開發中的八方走法實現<LoonFramework-Game>", 640, 480);
// 設定遊標
frame.setCursor(GameCursor.getCursor("image/cursor.png"));
// 遊戲地圖
RpgMap rpgMap = new RpgMap("image/map/maze.jpg",
"./image/map/maze.map");
// 顯示網格
rpgMap.showGrid(false);
// 創建主角
Role hero = new Role("image/role/gm.png", 7, 8, Config.DOWN, 0,
rpgMap);
hero.setName("媽媽說壞孩子長大以後就是GM");
hero.setPartyName("曾經的北S商人");
// 創建NPC1
Role npc1 = new Role("image/role/assassin.png", 3, 14,
Config.LEFT, 1, rpgMap);
npc1.setName("煉妖狐(爆刺、爆刺、一拍即死)");
npc1.setPartyName("此人已死,有事燒紙");
// //創建NPC2
Role npc2 = new Role("image/role/rogue.png", 15, 21, Config.LEFT,
1, rpgMap);
npc2.setName("貓貓(巴帽小偷)");
npc2.setPartyName("病貓不發威你還拿我當老虎了");
// 設定主角
rpgMap.setupHero(hero);
// 設定NPC
rpgMap.addRole(npc1);
rpgMap.addRole(npc2);
frame.getGame().setControl(new RpgLayout(rpgMap));
// 遊戲全屏
// frame.updateFullScreen();
frame.setFPS(true);
frame.mainLoop();
frame.showFrame();
}
});
}
}
示例截取圖如下所示:
本回提供的源碼內容包括:八方走法(支持鼠標及鍵盤)、地圖移動、角色碰撞、NPC隨機行動,詳細請下載參看.
至於腳本處理、事件觸發,對話框、場景轉換、角色對戰等部分將在以後逐步講解。
下載地址如下:[url]http://code.google.com/p/loon-simple/downloads/list[/url]
PS:這個jar有315KB(源碼在jar內),但代碼實際並不多,空間都是圖佔的……
下載地址如下:[url]http://code.google.com/p/loon-simple/downloads/list[/url]
PS:這個jar有315KB(源碼在jar內),但代碼實際並不多,空間都是圖佔的……
——————天上天下分割線—————
一直說寫JME部分,直到最近兩天才寫了些JME的介紹及例子,等五一整理下再發出來。(鄙人的懶性是很驚人的,比如07年底我就想裝個VS2008,結果到今天也沒安上……)
另外到我五一時準備把前一陣說過的TLOH發出來,這東西實際上就是非組件化的LoonFramework-Game應用,發的目的就是讓大家幫着改改,差不多就定形了,等到發LoonFramework-Game包時我可不想和某些東西似的不同版本間接口與函數都沒個連貫性……
再有隨着筆者手頭積累的Java遊戲代碼越來越多,功能越來越複雜,又產生了想寫個類似於RpgMakerXP的遊戲編輯器的念頭,於是業餘時間大體上是這樣度過的:找編輯器資料(學習其業務實現)-〉找解釋器(比如TVP使用的TJS腳本,參考其腳本模式)-〉下游戲(下載同人遊戲,學習其打包及應用模式)-〉玩遊戲(很多日寇的同人遊戲也讓人上癮,-_-|||),這樣一晃就三個多星期,筆者一直以來的致命缺點就是心太散,注意力超級不集中|||……
另外到我五一時準備把前一陣說過的TLOH發出來,這東西實際上就是非組件化的LoonFramework-Game應用,發的目的就是讓大家幫着改改,差不多就定形了,等到發LoonFramework-Game包時我可不想和某些東西似的不同版本間接口與函數都沒個連貫性……
再有隨着筆者手頭積累的Java遊戲代碼越來越多,功能越來越複雜,又產生了想寫個類似於RpgMakerXP的遊戲編輯器的念頭,於是業餘時間大體上是這樣度過的:找編輯器資料(學習其業務實現)-〉找解釋器(比如TVP使用的TJS腳本,參考其腳本模式)-〉下游戲(下載同人遊戲,學習其打包及應用模式)-〉玩遊戲(很多日寇的同人遊戲也讓人上癮,-_-|||),這樣一晃就三個多星期,筆者一直以來的致命缺點就是心太散,注意力超級不集中|||……