HTML5移動遊戲開發高階程式設計 9:自建Quintus引擎(1)

CopperDong發表於2018-02-18

9.1 引言

     現在的JavaScript非常適於當成一種互動式遊戲開發語言使用

9.2 建立可重用HTML5引擎的框架

     該引擎將被命名成Quintus。

     設計基本的引擎API:

  1. 需要在同一頁面上執行多個引擎例項,該需求確保引擎作為獨立單元出現,不會干擾自身或頁面的其他組成部分。
  2. 在可能的情況下,引擎應提供合理的選項預設值,以此免去搭建執行環境所需的大量配置
  3. 引擎應足夠靈活,對於簡單的例子和較複雜的遊戲來說都是可用的,且應支援不同的渲染引擎(比如Canvas、CSS、SVG和可能的WebGL等)。
      基本的引擎結構:

9.3 新增遊戲迴圈

      遊戲迴圈必須使用定時器來實現,定時器釋放JavaScript程式碼的控制權,把控制權交回給瀏覽器,這樣瀏覽器才能更新頁面和處理輸入事件。

      構建更好的遊戲迴圈定時器:使用requestAnimationFrame API

      傳統的遊戲迴圈使用兩大塊程式碼來執行每一幀,它們分別是更新程式碼和渲染程式碼。更新部分負責推進遊戲邏輯在一小段時間內的發展,處理任何的使用者輸入、移動和物件間碰撞,以及把每個遊戲物件更新成一致的狀態。

      然後,遊戲需要把自身渲染到螢幕上,至於以什麼樣的步驟進行渲染,這取決於遊戲的構建方式。就基於畫布的遊戲而言,通常你需要清除整塊畫布,重新在頁面上繪製所有必需的精靈。而對於CSS和SVG遊戲來說,只要正確更新了頁面上的物件的屬性,那麼工作實際上就算完成了---瀏覽器會負責物件的移動和更新。

9.4 新增繼承

    Quintus同時支援繼承和元件,採取了一種介於這兩者之間的折中做法,這使得你既能在一些地方合理使用繼承,又可在需要更多靈活性時使用元件。為了支援前者,引擎需要把一個傳統繼承模型新增到JavaScript中;為了支援後者,它需要增加一個元件系統,以及要擁有對事件的底層支援,這樣才能做到儘可能支援元件之間的解耦。

    一個最受歡迎的類層次結構是jQuery的建立者John Resig的Simple JavaScript繼承,其靈感來自base2和另一個名為Prototype.js的JavaScript庫。

var Person = Class.extend({
	init: function() {
		console.log('Created Person');
	},
	speak: function() {
		console.log('Person Speaking:');
	},
});
var p = new Person();
p.speak();

9.5 支援事件

    這便於避免引擎的不同部分過度緊密耦合。這意味著,在不必瞭解和遊戲進行通訊的物件的任何資訊的情況下,遊戲的一部分和其他部分之間能夠就事件和行為進行溝通。

    在把一些元件新增到這一混搭環境中後,它甚至允許精靈在不必瞭解構成自身的所有元件的情況下與自身通訊。精靈的某個物理元件可能會觸發一個碰撞事件,而兩個負責監聽該事件的元件則可以分別處理適當音響效果和動畫效果的觸發過程。

    設計事件API:使用了一個名為Evented的基類,這是任何需要訂閱和觸發事件的物件的起點。

9.6 支援元件

    元件簡化了小塊可重用功能的建立,這些可重用功能可經組合和匹配來滿足各種精靈和物件的需要。

    精靈元件的增加和刪除操作必須簡捷,它們應是可通過物件訪問的,但又不能過分汙染物件的名稱空間。

    需要註冊元件、新增元件、刪除元件,還允許使用其他一些方法來擴充基本精靈。

gameloop_test.html

<!DOCTYPE HTML>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title></title>
    <script src='jquery.min.js'></script>
    <script src='underscore.js'></script>
    <script src='quintus.js'></script>
  </head>
  <body>
    <div id='timer'>0</div>
    <div id='fps'>0</div>
    <button id='pause'>Pause</button>
    <button id='unpause'>Unpause</button>
    <script>
      var TimerTest = Quintus();

      var totalFrames = 0;
      var totalTime = 0.0;

      TimerTest.gameLoop(function(dt) {
      	totalTime += dt;
      	totalFrames += 1;
      	//console.log(totalTime);
      	//console.log(totalFrames);
      	$("#timer").text(Math.round(totalTime * 1000) + " MS");
      	$("#fps").text(Math.round(totalFrames / totalTime) + " FPS");
      });

      $("#pause").on('click', TimerTest.pauseGame);
      $("#unpause").on('click', TimerTest.unpauseGame);
    </script>
  </body>
</html>

quintus.js

// 繼承
(function(){
	var initializing = false;
	var fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
	this.Class = function(){};
	// Create a new Class that inherits from this class
	Class.extend = function(prop) {
		var _super = this.prototype;
		// Instantiate a base class
		initializing = true;
		var prototype = new this();
		initializing = false;
		// Copy the properties over onto the new prototype
		// 是否已存在於超類中,若是,則建立一個函式,在再次呼叫新方法之前
		// 該函式會先對this._super執行一個臨時的重新賦值。
		// 若該方法不存在,程式碼僅對屬性賦值,不會加入任何額外開銷。
		for (var name in prop) {
			// Check if we're
			prototype[name] = typeof prop[name] == "function" &&
			    typeof _super[name] == "function" && 
			    fnTest.test(prop[name]) ?
			    (function(name, fn) {
			    	return function() {
			    		var tmp = this._super;
			    		// Add a new ._super()
			    		this._super = _super[name];
			    		//
			    		var ret = fn.apply(this, arguments);
			    		this._super = tmp;
			    		return ret;
			    	};
			    })(name, prop[name]) : prop[name];
		}
		// 建構函式
		function Class() {
			// All construction is actually done in the init method
			if (!initializing && this.init) {
				this.init.apply(this, arguments);
			}
		}
		// Populate
		Class.prototype = prototype;
		// Enforce
		Class.prototype.constructor = Class;
		// make this class extendable
		Class.extend = arguments.callee;
		return Class;
	};
})();
// 新增遊戲迴圈
(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for (var x=0; x<vendors.length && !window.requestAnimationFrame; ++x) {
    	window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
    	window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||
    	   window[vendors[x] + 'CancelRequestAnimationFrame'];
    };
    if (!window.requestAnimationFrame) {
    	window.requestAnimationFrame = function(callback, element) {
    		var currTime = new Date().getTime();
    		var timeToCall = Math.max(0, 16 - (currTime - lastTime));
    		var id = window.setTimeout(function() { 
    			callback(currTime + timeToCall);
    		}, timeToCall);
    		lastTime = currTime + timeToCall;
    		return id;
    	};
    }
    if (!window.cancelAnimationFrame) {
    	window.cancelAnimationFrame = function(id) {
    		clearTimeout(id);
    	};
    }
}());

// 基本的引擎結構
var Quintus = function(opts) {
	var Q = {};
	// 
	Q.options = {
		// TODO:
	};
	if (opts) {
		_(Q.options).extend(opts);
	}
	Q._normalizeArg = function(arg) {
		if (_.isString(arg)) {
			arg = arg.replace(/\s+/g,'').split(",");
		}
		if (!_.isArray(arg)) {
			arg = [ arg ];
		}
		return arg;
	};
	// Shortcut to extend Quintus with new functionality
	// binding the methods to Q
	Q.extend = function(obj) {
		_(Q).extend(obj);
		return Q;
	};
	// Syntax for including other modules into quintus
	Q.include = function(mod) {
		_.each(Q._normalizeArg(mod), function(m) {
			m = Quintus[m] || m;
			m(Q);
		});
		return Q;
	};
	// 遊戲迴圈
	Q.gameLoop = function(callback) {
		Q.lastGameLoopFrame = new Date().getTime();
		Q.gameLoopCallbackWrapper = function(now) {
			Q.loop = requestAnimationFrame(Q.gameLoopCallbackWrapper);
			var dt = now - Q.lastGameLoopFrame;
			if (dt > 100) {
				dt = 100;
			}
			callback.apply(Q, [dt/1000]);
			Q.lastGameLoopFrame = now;
		};
		requestAnimationFrame(Q.gameLoopCallbackWrapper);
	};
	// 暫停遊戲
	Q.pauseGame = function() {
		if (Q.loop) {
			cancelAnimationFrame(Q.loop);
		}
		Q.loop = null;
	};
	// 取消暫停
	Q.unpauseGame = function() {
		if (!Q.loop) {
			Q.lastGameLoopFrame = new Date().getTime();
			Q.loop = requestAnimationFrame(Q.gameLoopCallbackWrapper);
		}
	}
	// 事件類
	Q.Evented = Class.extend({
		bind: function(event, target, callback) {
			if (!callback) {
				callback = target;
				target = null;
			}
			if (_.isString(classback)) {
				callback = target[callback];
			}
			this.listeners = this.listeners || {};
			this.listeners[event] = this.listeners[event] || [];
			this.listeners[event].push([target || this, callback]);
			if (target) {
				if (!target.binds) { 
					target.binds = [];
				}
				target.binds.push([this, event, callback]);
			}
		},
		// 觸發事件
		trigger: function(event, data) {
			if (this.listeners && this.listeners[event]) {
				for (var i=0,len=this.listeners[event].length; i<len; i++) {
					var listener = this.listeners[event][i];
					listener[1].call(listener[0], data);
				}
			}
		},
		unbind: function(event, target, callback) {
			if (!target) {
				if (this.listeners[event]) {
					delete this.listeners[event];
				}
			} else {
				var l = this.listeners && this.listeners[event];
				if (l) {
					for (var i=l.length-1; i>=0; i--) {
						if (l[i][0] == target) {
							if (!callback || callback == l[i][1]) {
								this.listeners[event].splice(i,1);
							}
						}
					}
				}
			}
		},
		// 銷燬物件時刪除其所有的監聽器
		debind: function() {
			if (this.binds) {
				for (var i=0,len=this.binds.length; i<len; i++) {
					var boundEvent = this.binds[i];
					var source = boundEvent[0];
					var event = boundEvent[1];
					source.unbind(event, this);
				}
			}
		},
	});
	// 元件
	Q.components = {};
	Q.register = function(name, methods) {
		methods.name = name;
		Q.components[name] = Q.Component.extend(methods);
	};
	Q.Component = Q.Evented.extend({
		init: function(entity) {
			this.entity = entity;
			if (this.extend) {
				_.extend(entity, this.extend);
			}
			entity[this.name] = this;
			entity.activeComponents.push(this.name);
			if (this.added)
				this.added();
		},
		destroy: function() {
			if (this.extend) {
				var extensions = _.keys(this.extend);
				for (var i=0,len=extensions.length; i<len; i++) {
					delete this.entity[extensions[i]];
				}
			}
			delete this.entity[this.name];
			var idx = this.entity.activeComponents.indexOf(this.name);
			if (idx != -1) {
				this.entity.activeComponents.splice(idx, 1);
			}
			this.debind();
			if (this.destroyed) this.destroyed();
		}
	});
	// 所有活動的遊戲物件的基類
	Q.GameObject = Q.Evented.extend({
		has: function(component) {
			return this[component] ? true : false;
		},
		add: function(components) {
			components = Q._normalizeArg(components);
			if (!this.activeComponents) {
				this.activeComponents = [];
			}
			for (var i=0,len=components.length; i<len; i++) {
				var name = components[i];
				var comp = Q.components[name];
				if (!this.has(name) && comp) {
					var c = new comp(this);
					this.trigger('addComponent', c);
				}
			}
			return this;
		},
		del: function(components) {
			components = Q._normalizeArg(components);
			for (var i=0,len=components.length; i<len; i++) {
				var name = components[i];
				if (name && this.has(name)) {
					this.trigger('delComponent', this[name]);
					this[name].destroy();
				}
			}
			return this;
		},
		destroy: function() {
			if (this.destroyed) {
				return;
			}
			this.debind();
			if (this.parent && this.parent.remove) {
				this.parent.remove(this);
			}
			this.trigger('removed');
			this.destroyed = true;
		}
	});
	return Q;
}
     你已經擁有了一些用來建立可重用的HTML5遊戲引擎的構建塊,你建立了最初的遊戲容器物件、遊戲迴圈以及一些使用事件和元件的基類。

相關文章