在遊戲的視覺效果定義其整體外觀、感覺和遊戲玩法本身。玩家被好的視覺體驗所吸引,從而可達到產生更多的流量。這是建立成功的遊戲和為玩家提供很多樂趣的關鍵。
在這篇文章中,我們基於 HTML5 遊戲的不同視覺效果實現,提出幾個構思方案。這些示例將依據我們自己的遊戲《Skytte 》所實現的效果。我會解釋支援他們的基本思想, ,並提供應用於我們專案中的效果。
你會學到什麼
在我們開始之前, 我想列出一些我希望你能從本文中學習的知識:
- 基本的遊戲設計
我們來看看通常用於製造遊戲和遊戲效果的模式: 遊戲迴圈、精靈、碰撞和粒子系統。 - 視覺效果的基本實現
我們還將探討支援這些模式的理論和一些程式碼示例。
常見的模式
讓我們從遊戲開發中常用的大一些模式和元素開始
精靈
這些只是在遊戲中代表一個物件的二維影象。精靈可以用於靜態物件, 也可以用於動畫物件, 當每個精靈代表一個幀序列動畫。它們也可用於製作使用者介面元素。
通常遊戲包含從幾十到幾百精靈圖片。為了減少記憶體的使用和處理這些映像所需的能力, 許多遊戲使用精靈表。
精靈表
這些都用來在一個影象中合成一套單個精靈。這減少了在遊戲中檔案的數量,從而減少記憶體和處理電源使用。精靈表包含許多單精靈堆積彼此相鄰的行和列,和類似精靈的影象檔案,它們包含可用於靜態或動畫。
精靈表例子。(影象來源: Kriplozoik)
下面是Code + Web的文章, 幫助您更好地理解使用精靈表的益處。
遊戲迴圈
重要的是要認識到遊戲物件並不真正在螢幕上移動。運動的假象是通過渲染一個遊戲世界的螢幕快照, 隨著遊戲的時間的一點點推進 (通常是1/60 秒), 然後再渲染的東西。這實際上是一個停止和運動的效果, 並常在二維和三 維遊戲中使用。遊戲迴圈是一種實現此停止運動的機制。它是執行遊戲所需的主要元件。它連續執行, 執行各種任務。在每個迭代中, 它處理使用者輸入, 移動實體, 檢查碰撞, 並渲染遊戲 (推薦按這個順序)。它還控制了幀之間的遊戲時間。
下面示例是用JavaScriptpgpg語言寫的非常基本的遊戲迴圈︰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var lastUpdate; function tick() { var now = window.Date.now(); if (lastUpdate) { var elapsed = (now-lastUpdate) / 1000; lastUpdate = now; // Update all game objects here. update(elapsed); // ...and render them somehow. render(); } else { // Skip first frame, so elapsed is not 0. lastUpdate = now; } // This makes the `tick` function run 60 frames per second (or slower, depends on monitor's refresh rate). window.requestAnimationFrame(tick); }; |
請注意,上面的例子中是非常簡單。它使用可變時間增量 (已用的變數),並建議升級此程式碼以使用固定的增量時間。有關詳細資訊, 請參閱本文。
碰撞檢測
碰撞檢測是指發現物體之間的交點。這對於許多遊戲是必不可少的, 因為它用來檢測玩家擊中牆壁或子彈擊中敵人, 諸如此類等等。當檢測到碰撞時, 它可以用於遊戲邏輯設計中;例如, 當子彈擊中玩家時, 健康分數會減少十點。
有很多碰撞檢測演算法, 因為它是一個效能繁重的操作, 明智的選擇最好的方法是很重要的。要了解有關碰撞檢測、演算法以及如何實現它們的更多資訊, 這裡有一篇來自MDN 的文章。
粒子和粒子系統
粒子基本上是用粒子系統的精靈。在遊戲開發中一個粒子系統是由粒子發射器和分配給該發射器的粒子組成的一個組成部分。它用來模擬各種特效,像火災、 爆炸、 煙、 和下雨的影響。隨著時間的推移微粒和每個發射器有其自身的引數來定義各種變數,用於模擬的效果,如速度、 顏色、 粒子壽命或持續時間,重力、 摩擦和風速。
尤拉積分
尤拉積分是運動的積分方程的一種方法。每個物件的位置計算基於其速度,質量和力量,並需要重新計算每個 tick 在遊戲迴圈。尤拉方法是最基本和最有用的像側滾動的射擊遊戲,但也有其它的方法,如Verlet 積分和 RK4積分,會更好地完成其他任務。下面我將展示一個簡單的實現的想法。
你需要一個基本的結構以容納物件的位置、 速度和其他運動相關的資料。我們提出兩個相同的結構,但每一個都有不同的意義,在世界空間中︰ 點和向量。遊戲引擎通常使用某種型別的向量類,但點和向量之間的區別是非常重要的,大大提高了程式碼的可讀性 (例如,您計算不是兩個向量,但這兩個點之間的距離,這是更自然)。
點
簡單地說, 它代表了二維空間空間中的一個元素, 它有 x 和 y 座標, 它定義了該點在該空間中的位置。
1 2 3 |
function point2(x, y) { return {'x': x || 0, 'y': y || 0}; } |
向量
一個向量是一個具有長度 (或大小) 的幾何物件和方向。2 D 遊戲中向量主要是用於描述力(例如重力、 空氣阻力和風) 和速度,以及禁止運動或光線反射。向量有許多用途。
1 2 3 |
function vector2(x, y) { return {'x': x || 0, 'y': y || 0}; } |
上述函式建立了新的二維向量和點。在這種情況下, 我們不會在 javascript 中使用 new 運算子來獲得大量的效能。還要注意, 有一些 第三方庫可用來操縱向量 (glMatrix 是一個很好的候選物件)。
下面是在上面定義的二維結構上使用的一些非常常用的函式。首先, 計算兩點之間的距離:
1 2 3 4 5 6 7 |
point2.distance = function(a, b) { // The x and y variables hold a vector pointing from point b to point a. var x = a.x - b.x; var y = a.y - b.y; // Now, distance between the points is just length (magnitude) of this vector, calculated like this: return Math.sqrt(x*x + y*y); }; |
向量的大小 (長度) 可以直接從最後一行的上面的函式,這樣計算︰
1 2 3 |
vector2.length = function(vector) { return Math.sqrt(vector.x*vector.x + vector.y*vector.y); }; |
向量的長度。
向量規範化也是非常方便的。下面的函式調整向量的大小,所以它成為一個單位向量;也就是說,它的長度是 1,但保持它的方向。
1 2 3 4 5 6 7 8 9 10 |
vector2.normalize = function(vector) { var length = vector2.length(vector); if (length > 0) { return vector2(vector.x / length, vector.y / length); } else { // zero-length vectors cannot be normalized, as they do not have direction. return vector2(); } }; |
向量歸一化。
另一個有用的例子是,其方向指從一個位置到另一個位置︰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Note that this function is different from `vector2.direction`. // Please don't confuse them. point2.direction = function(from, to) { var x = to.x - from.x; var y = to.y - from.y; var length = Math.sqrt(x*x + y*y); if (length > 0) { return vector2(x / length, y / length); } else { // `from` and `to` are identical return vector2(); } }; |
點積是對兩個向量 (通常為單位向量) 的運算, 它返回一個標量的數字, 表示這些向量的角度之間的關係。
1 2 3 |
vector2.dot = function(a, b) { return a.x*b.x + a.y*b.y; }; |
向量點積
點積是一個向量投影向量 b 上的長度。返回的值為 1 表示兩個向量指向同一方向。值為-1 意味著向量方向相反的向量 b 點。值為 0 表示該向量是垂直於向量 b。
這裡是實體類的示例,以便其他物件可以從它繼承。只描述了與運動相關的基本屬性。
1 2 3 4 5 6 7 8 9 10 11 12 |
function Entity() { ... // Center of mass usually. this.position = point2(); // Linear velocity. // There is also something like angular velocity, not described here. this.velocity = vector2(); // Acceleration could also be named `force`, like in the Box2D engine. this.acceleration = vector2(); this.mass = 1; ... } |
您可以在你的遊戲中使用畫素或米為單位。我們鼓勵您使用米,因為在開發過程中,它更容易平衡的事情。速度,應該是米每秒,而加速度應該是米每秒的平方。
當使用一個第三方物理引擎,只是將儲存在您的實體類的物理主體(或主體集) 的引用。然後,物理引擎將在每個主體記憶體儲所述的屬性,如位置和速度。
基本的尤拉積分看起來像這樣︰
1 2 3 |
acceleration = force / mass velocity += acceleration position += velocity |
上面的程式碼必須在遊戲中每個物件的每個幀中執行。下面是在 JavaScript 中的基本執行程式碼︰
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Entity.prototype.update = function(elapsed) { // Acceleration is usually 0 and is set from the outside. // Velocity is an amount of movement (meters or pixels) per second. this.velocity.x += this.acceleration.x * elapsed; this.velocity.y += this.acceleration.y * elapsed; this.position.x += this.velocity.x * elapsed; this.position.y += this.velocity.y * elapsed; ... this.acceleration.x = this.acceleration.y = 0; } |
經過的是自最後一個幀 (自最近一次呼叫此方法) 所經過的時間量 (以秒為單位)。對於執行在每秒 60 幀的遊戲,經過的值通常是 1/60 秒,也就是 0.016 (6) s。
上文提到的增量時間的文章也涵蓋了這個問題。
要移動物件,您可以更改其加速度或速度。為實現此目的,應使用如下所示的兩個函式︰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Entity.prototype.applyForce = function(force, scale) { if (typeof scale === 'undefined') { scale = 1; } this.acceleration.x += force.x * scale / this.mass; this.acceleration.y += force.y * scale / this.mass; }; Entity.prototype.applyImpulse = function(impulse, scale) { if (typeof scale === 'undefined') { scale = 1; } this.velocity.x += impulse.x * scale / this.mass; this.velocity.y += impulse.y * scale / this.mass; }; |
要向右移動一個物件你可以這樣做︰
1 2 3 4 5 6 7 8 9 |
// 10 meters per second in the right direction (x=10, y=0). var right = vector2(10, 0); if (keys.left.isDown) // The -1 inverts a vector, i.e. the vector will point in the opposite direction, // but maintain magnitude (length). spaceShip.applyImpulse(right, -1); if (keys.right.isDown) spaceShip.applyImpulse(right, 1); |
請注意,在運動中設定的物件保持運動。您需要實現某種減速停止移動的物體 (空氣阻力或摩擦,也許)。
武器的影響
現在我要解釋一下, 在我們的 HTML5 遊戲中, 某些武器效果是如何射擊的
等離子
在 Skytte中的等離子武器。
這是我們遊戲中最基本的武器, 每次都是一槍。沒有用於這種武器的特殊演算法。當等離子子彈發射時, 遊戲只需繪製一個隨著時間推移而旋轉的精靈。
簡單的等離子子彈可以催生像這樣︰
1 2 3 4 5 6 7 8 |
// PlasmaProjectile inherits from Entity class var plasma = new PlasmaProjectile(); // Move right (assuming that X axis is pointing right). var direction = vector2(1, 0); // 20 meters per second. plasma.applyImpulse(direction, 20); |
衝擊波
https://player.vimeo.com/video/135389040
在 Skytte 的衝擊波武器。
這種武器是更復雜一點。它也繪製簡單精靈作為子彈,但卻有一些程式碼,一點點傳播開,並應用隨機速度。這給這個武器帶來了更具破壞性的感覺,,所以玩家覺得他們可以施加比血漿武器更大的傷害, 並且在敵人中間有更好的控制人群。
該程式碼工作方式類似於血漿武器程式碼,但是它生成三發子彈,每個子彈都有一個稍微不同的方向。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// BlaserProjectile inherits from Entity class var topBullet = new BlasterProjectile(); // This bullet will move slightly up. var middleBullet = new BlasterProjectile(); // This bullet will move horizontally. var bottomBullet = new BlasterProjectile(); // This bullet will move slightly down. var direction; // Angle 0 is pointing directly to the right. // We start with the bullet moving slightly upwards. direction = vector2.direction(radians(-5)); // Convert angle to an unit vector topBullet.applyImpulse(direction, 30); direction = vector2.direction(radians(0)); middleBullet.applyImpulse(direction, 30); direction = vector2.direction(radians(5)); middleBullet.applyImpulse(direction, 30); |
上面的程式碼需要一些數學函式來實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function radians(angle) { return angle * Math.PI / 180; } // Note that this function is different from `point2.direction`. // Please don't confuse them. vector2.direction = function(angle) { /* * Converts an angle in radians to a unit vector. Angle of 0 gives vector x=1, y=0. */ var x = Math.cos(angle); var y = Math.sin(angle); return vector2(x, y); }; |
雷
https://player.vimeo.com/video/135389040
在 Skytte中雷武器。
這很有趣。武器射鐳射射線,但它在每個幀的程式生成 (這將在稍後解釋)。為了探測命中, 它會建立一個矩形對撞機, 它會在與敵人碰撞時每秒鐘造成傷害。
火箭
圖 8︰ 在 Skytte中火箭武器。
這種武器射導彈。火箭是一個精靈, 一個粒子發射器附著在它的末端。還有一些更復雜的邏輯,比如搜尋最近的敵人或限制火箭的轉彎值, 使其更少機動性。。此外,火箭就不會立即尋找敵方目標 — — 他們直接飛行一段時間, 以避免不切實際的行為。
火箭走向他們的相鄰的目標。這是通過計算彈丸在給定的方向移動所需的適當力量來實現的。為了避免只在直線上移動, 計算的力在 skytte不應該太大。
假設,火箭從前面所述的實體類繼承的類。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Rocket.prototype.update = function(elapsed) { var direction; if (this.target) { // Assuming that `this.target` points to the nearest enemy ship. direction = point2.direction(this.position, this.target.position); } else { // No target, so fly ahead. // This will fail for objects that are still, so remember to apply some initial velocity when spawning rockets. direction = vector2.normalize(this.velocity); } // You can use any number here, depends on the speed of the rocket, target and units used. this.applyForce(direction, 10); // Simple inheritance here, calling parent's `update()`, so rocket actually moves. Entity.prototype.update.apply(this, arguments); }; |
高射炮
在 Skytte 中高射炮武器。
高射炮被設計為射擊許多小子彈 (象獵槍), 是小斑點精靈。它有一些在錐形區域內的點的位置用特定的邏輯來隨機生成這些。
高射炮武器子彈錐區。
在一個圓錐形的區域中生成隨機點︰
1 2 3 4 5 6 7 8 9 10 11 12 |
// Firstly get random angle in degrees in the allowed span. Note that the span below always points to the right. var angle = radians(random.uniform(-40, 40)); // Now get how far from the barrel the projectile should spawn. var distance = random.uniform(5, 150); // Join angle and distance to create an offset from the gun's barrel. var direction = vector2.direction(angle); var offset = vector2(direction.x * distance, direction.y * distance); // Now calculate absolute position in the game world (you need a position of the barrel for this purpose): var position = point2.move(barrel, offset); |
函式返回兩個值之間的一個隨機浮點數。一個簡單的實現就像這個樣子︰
1 2 3 |
random.uniform = function(min, max) { return min + (max-min) * Math.random(); }; |
電
在 Skytte 中的電武器。
電是射擊在特定半徑範圍內的敵人的武器。它有一個有限的範圍, 但可以射擊在幾個敵人, 並總是射擊成功。它使用相同的演算法繪製曲線, 以模擬閃電作為射線武器, 但具有更高的曲線因子。
使用技術
產生彎曲的線條
為了製造鐳射束效應和電子武器, 我們開發了一種計算和變換玩家的艦船和敵人之間的直線距離的演算法。換句話說,我們測量的兩個物件之間的距離,找到中間點,並在這一段距離隨機移動它。我們為每個新場景建立重複此操作。
若要繪製這些部分我們使用 HTML5 繪製函式 lineTo()。為了實現發光顏色我們使用多行繪製到另一個更不透明的顏色和更高的描邊寬度。
程式上彎曲的線條。
要查詢並偏移其他兩個點之間的點︰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
var offset, midpoint; midpoint = point2.midpoint(A, B); // Calculate an unit-length vector pointing from A to B. offset = point2.direction(A, B); // Rotate this vector 90 degrees clockwise. offset = vector2.perpendicular(offset); // We want our offset to work in two directions perpendicular to the segment AB: up and down. if (random.sign() === -1) { // Rotate offset by 180 degrees. offset.x = -offset.x; offset.y = -offset.y; } // Move the midpoint by an offset. var offsetLength = Math.random() * 10; // Offset by 10 pixels for example. midpoint.x += offset.x * offsetLength; midpoint.y += offset.y * offsetLength; Below are functions used in the above code: point2.midpoint = function(a, b) { var x = (a.x+b.x) / 2; var y = (a.y+b.y) / 2; return point2(x, y); }; vector2.perpendicular = function(v) { /* * Rotates a vector by 90 degrees clockwise. */ return vector2(-v.y, v.x); }; random.sign = function() { return Math.random() < 0.5 ? -1 : 1; }; |
找到最近的相鄰目標
火箭和電武器找到最近的敵人,我們遍歷一群活躍的敵人並比較他們的位置與火箭的位置,或此專案中電武器射擊點。當火箭鎖定其目標,並會飛向目標時,直到它擊中目標或飛出螢幕。電武器,它會等待目標出現在範圍內。
一個基本的實現可能如下所示︰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function nearest(position, entities) { /* * Given position and an array of entites, this function finds which entity is closest * to `position` and distance. */ var distance, nearest = null, nearestDistance = Infinity; for (var i = 0; i < entities.length; i++) { // Allow list of entities to contain the compared entity and ignore it silently. if (position !== entities[i].position) { // Calculate distance between two points, usually centers of mass of each entity. distance = point2.distance(position, entities[i].position); if (distance < nearestDistance) { nearestDistance = distance; nearest = entities[i]; } } } // Return the closest entity and distance to it, as it may come handy in some situations. return {'entity': nearest, 'distance': nearestDistance}; } |
結論
這些主題涵蓋只支援它們的基本思路。我希望讀這篇文章後,你對如何開始並持續發展遊戲專案會有更好的主意。查閱下面的參考,你可以自己試著做類似的遊戲專案。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式