手把手教你建立一個(gè)Java游戲引擎

今天,讓我們進(jìn)入一個(gè)可以伸手觸摸的世界吧铛只。在這篇文章里埠胖,我們將從零開始快速完成一次第一人稱探索。本文沒有涉及復(fù)雜的數(shù)學(xué)計(jì)算淳玩,只用到了光線投射技術(shù)直撤。你可能已經(jīng)見識過這種技術(shù)了,比如《上古卷軸5 : 天際》蜕着、《毀滅公爵3D》谋竖。

用了光線投射就像開掛一樣,作為一名懶得出油的程序員承匣,我表示非常喜歡蓖乘。你可以舒暢地浸入到3D環(huán)境中而不受“真3D”復(fù)雜性的束縛。舉例來說韧骗,光線投射算法消耗線性時(shí)間嘉抒,所以不用優(yōu)化也可以加載一個(gè)巨大的世界,它執(zhí)行的速度跟小型世界一樣快袍暴。水平面被定義成簡單的網(wǎng)格而不是多邊形網(wǎng)面樹些侍,所以即使沒有 3D 建奈漳粒基礎(chǔ)或數(shù)學(xué)博士學(xué)位也可以直接投入進(jìn)去學(xué)習(xí)。

利用這些技巧很容易就可以做一些讓人嗨爆的事情娩梨。15分鐘之后沿腰,你會(huì)到處拍下你辦公室的墻壁,然后檢查你的 HR 文檔看有沒有規(guī)則禁止“工作場所槍戰(zhàn)建谋范ǎ”颂龙。

玩家

我們從何處投射光線?這就是玩家對象(Player)的作用纽什,只需要三個(gè)屬性 x措嵌,y,direction芦缰。

JavaScript

function Player(x, y, direction) {
  this.x = x;
  this.y = y;
  this.direction = direction;
}

function Player(x, y, direction) {
  this.x = x;
  this.y = y;
  this.direction = direction;
}

地圖

我們將地圖存作簡單的二維數(shù)組企巢。數(shù)組中,0代表沒墻浪规,1代表有墻。你還可以做得更復(fù)雜些探孝,比如給墻設(shè)任意高度笋婿,或者將多個(gè)墻數(shù)據(jù)的“樓層(stories)”打包進(jìn)數(shù)組。但作為我們的第一次嘗試顿颅,用0-1就足夠了缸濒。

JavaScript

function Map(size) {
  this.size = size;
  this.wallGrid = new Uint8Array(size * size);
}

function Map(size) {
  this.size = size;
  this.wallGrid = new Uint8Array(size * size);
}

投射一束光線

這里就是竅門:光線投射引擎不會(huì)一次性繪制出整個(gè)場景。相反粱腻,它把場景分成獨(dú)立的列然后一條一條地渲染庇配。每一列都代表從玩家特定角度投射出的一條光線。如果光線碰到墻壁绍些,引擎會(huì)計(jì)算玩家到墻的距離然后在該列中畫出一個(gè)矩形捞慌。矩形的高度取決于光線的長度——越遠(yuǎn)則越短。

繪畫的光線越多遇革,顯示效果就會(huì)越平滑卿闹。

1. 找到每條光線的角度

我們首先找出每條光線投射的角度。角度取決于三點(diǎn):玩家面向的方向萝快,攝像機(jī)的視野锻霎,還有正在繪畫的列。

JavaScript

var angle = this.fov * (column / this.resolution - 0.5);
var ray = map.cast(player, player.direction + angle, this.range);

var angle = this.fov * (column / this.resolution - 0.5);
var ray = map.cast(player, player.direction + angle, this.range);

2. 通過網(wǎng)格跟蹤每條光線

接下來揪漩,我們要檢查每條光線經(jīng)過的墻旋恼。這里的目標(biāo)是最終得出一個(gè)數(shù)組,列出了光線離開玩家后經(jīng)過的每面墻奄容。

從玩家開始冰更,我們找出最接近的橫向(stepX)和縱向(stepY)網(wǎng)格坐標(biāo)線产徊。移到最近的地方然后檢查是否有墻(inspect)。一直重復(fù)檢查直到跟蹤完每條線的所有長度蜀细。

JavaScript

function ray(origin) {
  var stepX = step(sin, cos, origin.x, origin.y);
  var stepY = step(cos, sin, origin.y, origin.x, true);
  var nextStep = stepX.length2 < stepY.length2
    ? inspect(stepX, 1, 0, origin.distance, stepX.y)
    : inspect(stepY, 0, 1, origin.distance, stepY.x);

  if (nextStep.distance > range) return [origin];
  return [origin].concat(ray(nextStep));
}

function ray(origin) {
  var stepX = step(sin, cos, origin.x, origin.y);
  var stepY = step(cos, sin, origin.y, origin.x, true);
  var nextStep = stepX.length2 < stepY.length2
    ? inspect(stepX, 1, 0, origin.distance, stepX.y)
    : inspect(stepY, 0, 1, origin.distance, stepY.x);
 
  if (nextStep.distance > range) return [origin];
  return [origin].concat(ray(nextStep));
}

尋找網(wǎng)格交點(diǎn)很簡單:只需要對 x 向下取整(1,2,3…)舟铜,然后乘以光線的斜率(rise/run)得出 y。

JavaScript

var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x;
var dy = dx * (rise / run);

var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x;
var dy = dx * (rise / run);

現(xiàn)在看出了這個(gè)算法的亮點(diǎn)沒有奠衔?我們不用關(guān)心地圖有多大谆刨!只需要關(guān)注網(wǎng)格上特定的點(diǎn)——與每幀的點(diǎn)數(shù)大致相同。樣例中的地圖是32×32归斤,而32,000×32,000的地圖一樣跑得這么快痊夭!

3. 繪制一列

跟蹤完一條光線后,我們就要畫出它在路徑上經(jīng)過的所有墻脏里。

JavaScript

var z = distance * Math.cos(angle);
var wallHeight = this.height * height / z;

var z = distance * Math.cos(angle);
var wallHeight = this.height * height / z;

我們通過墻高度的最大除以 z 來覺得它的高度抄淑。越遠(yuǎn)的墻缰盏,就畫得越短滑凉。

額吨凑,這里用 cos 是怎么回事?如果直接使用原來的距離员淫,就會(huì)產(chǎn)生一種超廣角的效果(魚眼鏡頭)合蔽。為什么?想象你正面向一面墻介返,墻的左右邊緣離你的距離比墻中心要遠(yuǎn)。于是原本直的墻中心就會(huì)膨脹起來了沃斤!為了以我們真實(shí)所見的效果去渲染墻面圣蝎,我們通過投射的每條光線一起構(gòu)建了一個(gè)三角形,通過 cos 算出垂直距離衡瓶。如圖:

我向你保證徘公,這里已經(jīng)是本文最難的數(shù)學(xué)啦。

渲染出來

我們用攝像頭對象 Camera 從玩家視角畫出地圖的每一幀哮针。當(dāng)我們從左往右掃過屏幕時(shí)它會(huì)負(fù)責(zé)渲染每一列关面。

在繪制墻壁之前,我們先渲染一個(gè)天空盒(skybox)——就是一張大的背景圖十厢,有星星和地平線等太,畫完墻后我們還會(huì)在前景放個(gè)武器。

JavaScript

Camera.prototype.render = function(player, map) {
  this.drawSky(player.direction, map.skybox, map.light);
  this.drawColumns(player, map);
  this.drawWeapon(player.weapon, player.paces);
};

Camera.prototype.render = function(player, map) {
  this.drawSky(player.direction, map.skybox, map.light);
  this.drawColumns(player, map);
  this.drawWeapon(player.weapon, player.paces);
};

攝像機(jī)最重要的屬性是分辨率(resolution)蛮放、視野(fov)和射程(range)缩抡。

  • 分辨率決定了每幀要畫多少列,即要投射多少條光線包颁。
  • 視野決定了我們能看的寬度瞻想,即光線的角度压真。
  • 射程決定了我們能看多遠(yuǎn),即光線長度的最大值

組合起來

使用控制對象 Controls 監(jiān)聽方向鍵(和觸摸事件)蘑险。使用游戲循環(huán)對象 GameLoop 調(diào)用 requestAnimationFrame 請求渲染幀滴肿。
這里的 gameloop 只有三行

JavaScript

oop.start(function frame(seconds) {
  map.update(seconds);
  player.update(controls.states, map, seconds);
  camera.render(player, map);
});

oop.start(function frame(seconds) {
  map.update(seconds);
  player.update(controls.states, map, seconds);
  camera.render(player, map);
});

細(xì)節(jié)

雨滴

雨滴是用大量隨機(jī)放置的短墻模擬的。

JavaScript

var rainDrops = Math.pow(Math.random(), 3) * s;
var rain = (rainDrops > 0) && this.project(0.1, angle, step.distance);

ctx.fillStyle = '#ffffff';
ctx.globalAlpha = 0.15;
while (--rainDrops > 0) ctx.fillRect(left, Math.random() * rain.top, 1, rain.height);

var rainDrops = Math.pow(Math.random(), 3) * s;
var rain = (rainDrops > 0) && this.project(0.1, angle, step.distance);
 
ctx.fillStyle = '#ffffff';
ctx.globalAlpha = 0.15;
while (--rainDrops > 0) ctx.fillRect(left, Math.random() * rain.top, 1, rain.height);

這里沒有畫出墻完全的寬度佃迄,而是畫了一個(gè)像素點(diǎn)的寬度嘴高。

照明和閃電

照明其實(shí)就是明暗處理。所有的墻都是以完全亮度畫出來和屎,然后覆蓋一個(gè)帶有一定不透明度的黑色矩形拴驮。不透明度決定于距離與墻的方向(N/S/E/W)。

JavaScript

ctx.fillStyle = '#000000';
ctx.globalAlpha = Math.max((step.distance + step.shading) / this.lightRange - map.light, 0);
ctx.fillRect(left, wall.top, width, wall.height);

ctx.fillStyle = '#000000';
ctx.globalAlpha = Math.max((step.distance + step.shading) / this.lightRange - map.light, 0);
ctx.fillRect(left, wall.top, width, wall.height);

要模擬閃電柴信,map.light 隨機(jī)達(dá)到2然后再快速地淡出套啤。

碰撞檢測

要防止玩家穿墻,我們只要用他要到的位置跟地圖比較随常。分開檢查 x 和 y 玩家就可以靠著墻滑行潜沦。

JavaScript

Player.prototype.walk = function(distance, map) {
  var dx = Math.cos(this.direction) * distance;
  var dy = Math.sin(this.direction) * distance;
  if (map.get(this.x + dx, this.y) <= 0) this.x += dx;
  if (map.get(this.x, this.y + dy) <= 0) this.y += dy;
};

Player.prototype.walk = function(distance, map) {
  var dx = Math.cos(this.direction) * distance;
  var dy = Math.sin(this.direction) * distance;
  if (map.get(this.x + dx, this.y) <= 0) this.x += dx;
  if (map.get(this.x, this.y + dy) <= 0) this.y += dy;
};

墻壁貼圖

沒有貼圖(texture)的墻面看起來會(huì)比較無趣。但我們怎么把貼圖的某個(gè)部分對應(yīng)到特定的列上绪氛?這其實(shí)很簡單:取交叉點(diǎn)坐標(biāo)的小數(shù)部分唆鸡。

JavaScript

step.offset = offset - Math.floor(offset);
var textureX = Math.floor(texture.width * step.offset);

step.offset = offset - Math.floor(offset);
var textureX = Math.floor(texture.width * step.offset);

舉例來說,一面墻上的交點(diǎn)為(10,8.2)枣察,于是取小數(shù)部分0.2争占。這意味著交點(diǎn)離墻左邊緣20%遠(yuǎn),離墻右邊緣80%遠(yuǎn)序目。所以我們用 0.2 * texture.width 得出貼圖的 x 坐標(biāo)臂痕。

試一試

  • 在恐怖廢墟中逛一逛。
  • 還有人擴(kuò)展了社區(qū)版猿涨。
  • ctolsen添加了 WASD 方向鍵握童。
  • Fredrik Wallgren 實(shí)現(xiàn)了 Java 移植。

接下來做什么叛赚?

因?yàn)楣饩€投射器是如此地快速澡绩、簡單,你可以快速地實(shí)現(xiàn)許多想法俺附。你可以做個(gè)地牢探索者(Dungeon Crawler)肥卡、第一人稱射手、或者俠盜飛車式沙盒昙读≌俚鳎靠!常數(shù)級的時(shí)間消耗真讓我想做一個(gè)老式的大型多人在線角色扮演游戲,包含大量的唠叛、程序自動(dòng)生成的世界只嚣。這里有一些帶你起步的難題:

  • 浸入式體驗(yàn)。樣例在求你為它加上全屏艺沼、鼠標(biāo)定位册舞、下雨背景和閃電時(shí)同時(shí)出現(xiàn)雷響。
  • 室內(nèi)級別障般。用對稱漸變?nèi)〈炜蘸械骶ā;蛘咄斓矗阌X得自己很屌的話藐石,嘗試用瓷片渲染地板和天花板。(可以這么想:所有墻面畫出來之后定拟,畫面剩下的空隙就是地板和天花板了)
  • 照明對象于微。我們已經(jīng)有了一個(gè)相當(dāng)健壯的照明模型。為何不將光源放到地圖上青自,通過它們計(jì)算墻的照明株依?光源占了80%大氣層。
  • 良好的觸摸事件延窜。我已經(jīng)搞定了一些基本的觸摸操作恋腕,手機(jī)和平板的小伙伴們可以嘗試一樣 demo。但這里還有巨大的提升空間逆瑞。
  • 攝像機(jī)特效荠藤。比如放大縮小、模糊呆万、醉漢模式等等商源。有了光線投射器這些都顯得特別簡單。先從控制臺(tái)中修改 camera.fov 開始谋减。

心動(dòng)了嗎?還不趕緊動(dòng)起來扫沼,打造屬于自己的游戲世界出爹!頓時(shí)滿滿的自豪感,真的很想知道大家的想法缎除,還請持續(xù)關(guān)注更新严就,更多干貨和資料請直接聯(lián)系我,也可以加群710520381器罐,邀請碼:柳貓梢为,歡迎大家共同討論

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子铸董,更是在濱河造成了極大的恐慌祟印,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粟害,死亡現(xiàn)場離奇詭異蕴忆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)悲幅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門套鹅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人汰具,你說我怎么就攤上這事卓鹿。” “怎么了留荔?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵吟孙,是天一觀的道長。 經(jīng)常有香客問我存谎,道長拔疚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任既荚,我火速辦了婚禮稚失,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘恰聘。我一直安慰自己句各,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布晴叨。 她就那樣靜靜地躺著凿宾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪兼蕊。 梳的紋絲不亂的頭發(fā)上初厚,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天,我揣著相機(jī)與錄音孙技,去河邊找鬼产禾。 笑死,一個(gè)胖子當(dāng)著我的面吹牛牵啦,可吹牛的內(nèi)容都是我干的亚情。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼哈雏,長吁一口氣:“原來是場噩夢啊……” “哼楞件!你這毒婦竟也來了衫生?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤土浸,失蹤者是張志新(化名)和其女友劉穎罪针,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體栅迄,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡站故,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了毅舆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片西篓。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖憋活,靈堂內(nèi)的尸體忽然破棺而出岂津,到底是詐尸還是另有隱情,我是刑警寧澤悦即,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布吮成,位于F島的核電站,受9級特大地震影響辜梳,放射性物質(zhì)發(fā)生泄漏粱甫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一作瞄、第九天 我趴在偏房一處隱蔽的房頂上張望茶宵。 院中可真熱鬧,春花似錦宗挥、人聲如沸乌庶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞒大。三九已至,卻和暖如春搪桂,著一層夾襖步出監(jiān)牢的瞬間透敌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工踢械, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拙泽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓裸燎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親泼疑。 傳聞我的和親對象是個(gè)殘疾皇子德绿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 今天是周二。早上去了周亦卿樓上UML課,這似乎是倒數(shù)第二節(jié)了移稳,忙碌的一個(gè)學(xué)期進(jìn)入了最后的收尾階段蕴纳,時(shí)間真是飛快。 ...
    焦大仙閱讀 272評論 0 0
  • 有夢想的人是一個(gè)矢量个粱,雖然他們能力有大小古毛,但是他們都有方向,即使被移到哪里都许,永遠(yuǎn)指向心中的矢點(diǎn)稻薇。
    乾立風(fēng)中閱讀 212評論 0 0
  • 互聯(lián)網(wǎng)運(yùn)營跟很多領(lǐng)域都存在著模糊的定義,特別是跟產(chǎn)品運(yùn)營很多相同的點(diǎn)胶征,特別是在同樣是面對用戶的時(shí)候塞椎,運(yùn)營和產(chǎn)品又是...
    大洲ss閱讀 211評論 2 0
  • 集體討論練習(xí):反思接下來的陳述。它有道理嗎睛低?你在本章所讀到的任何內(nèi)容對解釋它有所幫助嗎案狠?如果有,為什么钱雷?和兩個(gè)或者...
    鄧潔兒閱讀 76評論 0 0
  • 課后作業(yè)1. 對于這節(jié)課學(xué)習(xí)罩抗,我收獲到什么拉庵?對于這些收獲,我有怎樣的感受澄暮?這些收獲對于我的價(jià)值是什么名段?與我的生活有...
    王芳蘭閱讀 461評論 0 0