【轉】265行代碼實現第一人稱遊戲引擎

來源:互聯網
上載者:User

標籤:

原文:html5gamedev.org/?p=2383

總在有個自以為很先進的理念後不久就在網上找到別人的實現!

 

今天,讓我們進入一個可以伸手觸摸的世界吧。在這篇文章裡,我們將從零開始快速完成一次第一人稱探索。本文沒有涉及複雜的數學計算,只用到了光線投射技術。你可能已經見識過這種技術了,比如《上古捲軸2 : 匕首雨》、《毀滅公爵3D》還有 Notch Persson 最近在 ludum dare 上的參賽作品。Notch 認為它夠好,我就認為它夠好!

用了光線投射就像開掛一樣,作為一名懶得出油的程式員,我表示非常喜歡。你可以舒暢地浸入到3D環境中而不受“真3D”複雜性的束縛。舉例來說,光線投射演算法消耗線性時間,所以不用最佳化也可以載入一個巨大的世界,它執行的速度跟小型世界一樣快。水平面被定義成簡單的網格而不是多邊形網面樹,所以即使沒有 3D 建模基礎或數學博士學位也可以直接投入進去學習。

利用這些技巧很容易就可以做一些讓人嗨爆的事情。15分鐘之後,你會到處拍下你辦公室的牆壁,然後檢查你的 HR 文檔看有沒有規則禁止“工作場所槍戰建模”

 

玩家

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

 

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

我們將地圖存作簡單的二維數組。數組中,0代表沒牆,1代表有牆。你還可以做得更複雜些,比如給牆設任意高度,或者將多個牆資料的“樓層(stories)”打包進數組。但作為我們的第一次嘗試,用0-1就足夠了。

?
1234 function Map(size) {  this.size = size;  this.wallGrid = new Uint8Array(size * size);}
投射一束光線

這裡就是竅門:光線投射引擎不會一次性繪製出整個情境。相反,它把情境分成獨立的列然後一條一條地渲染。每一列都代表從玩家特定角度投射出的一條光線。如果光線碰到牆壁,引擎會計算玩家到牆的距離然後在該列中畫出一個矩形。矩形的高度取決於光線的長度——越遠則越短。

繪畫的光線越多,顯示效果就會越平滑。

 1. 找到每條光線的角度

我們首先找出每條光線投射的角度。角度取決於三點:玩家面向的方向,攝像機的視野,還有正在繪畫的列。

?
12 var angle = this.fov * (column / this.resolution - 0.5);var ray = map.cast(player, player.direction + angle, this.range);
2. 通過網格跟蹤每條光線

接下來,我們要檢查每條光線經過的牆。這裡的目標是最終得出一個數組,列出了光線離開玩家後經過的每面牆。

從玩家開始,我們找出最接近的橫向(stepX)和縱向(stepY)網格座標線。移到最近的地方然後檢查是否有牆(inspect)。一直重複檢查直到跟蹤完每條線的所有長度。

?
12345678910 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));}

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

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

現在看出了這個演算法的亮點沒有?我們不用關心地圖有多大!只需要關注網格上特定的點——與每幀的點數大致相同。範例中的地圖是32×32,而32,000×32,000的地圖一樣跑得這麼快!

 3. 繪製一列

跟蹤完一條光線後,我們就要畫出它在路徑上經過的所有牆。

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

我們通過牆高度的最大除以 z 來覺得它的高度。越遠的牆,就畫得越短。

額,這裡用 cos 是怎麼回事?如果直接使用原來的距離,就會產生一種超廣角的效果(魚眼鏡頭)。為什嗎?想象你正面向一面牆,牆的左右邊緣離你的距離比牆中心要遠。於是原本直的牆中心就會膨脹起來了!為了以我們真實所見的效果去渲染牆面,我們通過投射的每條光線一起構建了一個三角形,通過 cos 算出垂直距離。

我向你保證,這裡已經是本文最難的數學啦。

渲染出來

我們用網路攝影機對象 Camera 從玩家視角畫出地圖的每一幀。當我們從左往右掃過螢幕時它會負責渲染每一列。

 在繪製牆壁之前,我們先渲染一個天空盒(skybox)——就是一張大的背景圖,有星星和地平線,畫完牆後我們還會在前景放個武器。?
12345 Camera.prototype.render = function(player, map) {  this.drawSky(player.direction, map.skybox, map.light);  this.drawColumns(player, map);  this.drawWeapon(player.weapon, player.paces);};

攝像機最重要的屬性是解析度(resolution)、視野(fov)和射程(range)。

  • 解析度決定了每幀要畫多少列,即要投射多少條光線。
  • 視野決定了我們能看的寬度,即光線的角度。
  • 射程決定了我們能看多遠,即光線長度的最大值
組合起來

使用控制對象 Controls 監聽方向鍵(和觸摸事件)。使用遊戲迴圈對象 GameLoop 調用 requestAnimationFrame 請求渲染幀。這裡的 gameloop 只有三行

?
12345 oop.start(function frame(seconds) {  map.update(seconds);  player.update(controls.states, map, seconds);  camera.render(player, map);});
細節雨滴

雨滴是用大量隨機放置的短牆類比的。

?
123456 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);

這裡沒有畫出牆完全的寬度,而是畫了一個像素點的寬度。

照明和閃電

照明其實就是明暗處理。所有的牆都是以完全亮度畫出來,然後覆蓋一個帶有一定不透明度的黑色矩形。不透明度決定於距離與牆的方向(N/S/E/W)。

?
123 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 隨機達到2然後再快速地淡出。

碰撞檢測

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

?
123456 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)的牆面看起來會比較無趣。但我們怎麼把貼圖的某個部分對應到特定的列上?這其實很簡單:取交叉點座標的小數部分。

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

舉例來說,一面牆上的交點為(10,8.2),於是取小數部分0.2。這意味著交點離牆左邊緣20%遠(8),離牆右邊緣80%遠(9)。所以我們用 0.2 * texture.width 得出貼圖的 x 座標。

試一試在恐怖廢墟中逛一逛。

 

接下來做什嗎?

因為光線投射器是如此地快速、簡單,你可以快速地實現許多想法。你可以做個地牢探索者(Dungeon Crawler)、第一人稱射手、或者俠盜飛車式沙箱。靠!常數級的時間消耗真讓我想做一個老式的大型多人線上角色扮演遊戲,包含大量的、程式自動產生的世界。這裡有一些帶你起步的難題:

  • 浸入式體驗。範例在求你為它加上全屏、滑鼠定位、下雨背景和閃電時同時出現雷響。
  • 室內層級。用對稱漸層取代天空盒。或者,你覺得自己很屌的話,嘗試用瓷片渲染地板和天花板。(可以這麼想:所有牆面畫出來之後,畫面剩下的空隙就是地板和天花板了)
  • 照明對象。我們已經有了一個相當健壯的照明模型。為何不將光源放到地圖上,通過它們計算牆的照明?光源佔了80%大氣層。
  • 良好的觸摸事件。我已經搞定了一些基本的觸摸操作,手機和平板的小夥伴們可以嘗試一樣 demo。但這裡還有巨大的提升空間。
  • 攝像機特效。比如放大縮小、模糊、醉漢模式等等。有了光線投射器這些都顯得特別簡單。先從控制台中修改 camera.fov 開始。

源碼地址:https://github.com/hunterloftis/playfuljs/

原文連結: http://www.playfuljs.com/a-first-person-engine-in-265-lines

 

【轉】265行代碼實現第一人稱遊戲引擎

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.