最近webgl比较流行,three.js又是一个比较知名的开源库,作为一个unity开发我也来学习学习。
然后,提供一种Unity这样的实体-组件-系统的开发方式。
框架代码如下:(我取名叫ThreeECS)
/// <reference path="three.js" /> // 基于three.js的ECS框架 // yangxun //==================ESC驱动系统========================= function ThreeECS() { this.scene = new THREE.Scene(); //// this.scene.background = new THREE.Color(0,0,0,0); this.renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), alpha: true }); this.renderer.setClearAlpha(0) this.renderer.antialias = true; this.renderer.setPixelRatio(window.devicePixelRatio); //this.renderer.gammaOutput = true; this.isInited = false; } var preCallbacks = new Array(); var updateCallbacks = new Array(); var nextCallbacks = new Array(); function Loop() { requestAnimationFrame(Loop); for (var i = 0; i < preCallbacks.length; i++) { preCallbacks[i](); } preCallbacks.length = 0; for (var i = 0; i < updateCallbacks.length; i++) { updateCallbacks[i](); } for (var i = 0; i < nextCallbacks.length; i++) { nextCallbacks[i](); } nextCallbacks.length = 0; YxTime.frames++; } function AddToHtml() { if (this.isInited) { return; } this.renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(this.renderer.domElement); ThreeECS.Instance = this; this.gameObjects = new Array(); YxInput.Init(); this.isInited = true; Loop(); window.addEventListener('resize', function () { if (YxCamera.HasCamera()) { for (var i = 0; i < YxCamera.array.length; i++) { var cam = YxCamera.array[i].threeObj; cam.aspect = window.innerWidth / window.innerHeight; cam.updateProjectionMatrix(); } } ThreeECS.Instance.renderer.setSize(window.innerWidth, window.innerHeight); }, false); function render() { ThreeECS.Instance.renderer.clear(); if (YxCamera.HasCamera()) { for (var i = 0; i < YxCamera.array.length; i++) { var cam = YxCamera.array[i]; ThreeECS.Instance.renderer.render(ThreeECS.Instance.scene, YxCamera.array[i].threeObj); } } for (var i = 0; i < ThreeECS.Instance.gameObjects.length; i++) { var g = ThreeECS.Instance.gameObjects[i]; if (g.active === true && !g.isDisposed) { for (var j = 0; j < g.component.length; j++) { var c = g.component[j]; if (c.Started === false) { c.Started = true; if (c.Start != null) { c.Start(); } } } } } for (var i = 0; i < ThreeECS.Instance.gameObjects.length; i++) { var g = ThreeECS.Instance.gameObjects[i]; if (g.active === true && !g.isDisposed) { for (var j = 0; j < g.component.length; j++) { var c = g.component[j]; if (c.Started && c.Update != null) { c.Update(); } } } } for (var i = ThreeECS.Instance.gameObjects.length - 1; i >= 0; i--) { var n = ThreeECS.Instance.gameObjects[i]; if (n === null || n.isDisposed) { ThreeECS.Instance.gameObjects.splice(i, 1); } } for (var i = 0; i < ThreeECS.Instance.gameObjects.length; i++) { var g = ThreeECS.Instance.gameObjects[i]; if (g.active === true) { for (var j = 0; j < g.component.length; j++) { var c = g.component[j]; if (c.Started && c.LateUpdate != null) { c.LateUpdate(); } } } } } updateCallbacks.push(render); } Object.assign(ThreeECS.prototype, { AddToHtml: AddToHtml }); //==========GameObject实体,ECS中的基本类型=============== function YxGameObject(name, threeObj) { this.name = name; this.threeObj = threeObj; if (this.threeObj != null) { this.threeObj.name = name; this.threeObj.yxproxy = this; } this.active = true; this.isDisposed = false; this.component = new Array(); if (ThreeECS.Instance === null) { console.log("ThreeECS还没有附加到html"); } else { ThreeECS.Instance.gameObjects.push(this); if (this.threeObj != null) { ThreeECS.Instance.scene.add(this.threeObj); this.active = true; } } this.SetActive = function (enable) { if (this.active === enable) { return; } this.active = enable; if (enable) { ThreeECS.Instance.scene.add(this.threeObj); } else { ThreeECS.Instance.scene.remove(this.threeObj); } } this.AddComponent = function (com) { if (com != null) { this.component.push(com); com.gameObject = this; if (com.Awake != null) { com.Awake(); } com.Started = false; return com; } } this.GetComponentById = function(yxid){ for (var i = 0; i < this.component.length; i++) { if (this.component[i].yxid === yxid) { return this.component[i]; } } } this.RemoveComponent = function (com) { if (com != null) { this.component.splice(com); if (com.OnDestroy != null) { com.OnDestroy(); } } } this.Destroy = function () { if (this.isDisposed) { console.log(this.name + "对象已经被释放"); return; } if (this.threeObj != null && this.active) { ThreeECS.Instance.scene.remove(this.threeObj); } for (var i = 0; i < this.component.length; i++) { var c = this.component[i]; if (c != null && c.OnDestroy != null) { c.OnDestroy(); c = null; } } if (this.threeObj != null) { this.threeObj = null; } this.component = null; this.isDisposed = true; } } YxGameObject.Find = function (name) { var gos = ThreeECS.Instance.gameObjects; for (var i = 0; i < gos.length; i++) { if (gos[i].name === name) { return gos[i]; } } } YxGameObject.GetProxy = function (threeObj) { var p = threeObj; do { if (p.yxproxy != undefined && p.yxproxy != null) { return p.yxproxy; } p = p.parent; } while (p != undefined && p != null) return null; } //==========Camera实体,继承自YxCameObject================ function YxCamera(name,camera, depth) { YxGameObject.call(this); this.name = name; this.threeObj = camera; this.depth = depth; YxCamera.array.push(this); YxCamera.array.sort(function (a, b) { return a.depth <= b.depth; }); //渲染范围,暂未实现 var camRect; this.SetRect = function (rect) { camRect = rect; }; this.GetRect = function () { return camRect; } this.Destroy = function () { this.threeObj.dispose(); this.threeObj = null; YxCamera.array.splice(this); YxCamera.array.sort(function (a, b) { return a.depth <= b.depth; }); } } ( function () { var Super = function () { }; Super.prototype = YxGameObject.prototype; YxCamera.prototype = new Super(); YxCamera.prototype.constructor = YxCamera; } )(); YxCamera.array = new Array(); YxCamera.HasCamera = function(){ return YxCamera.array.length > 0; } YxCamera.Find = function (name) { var cams = YxCamera.array; for (var i = 0; i < cams.length; i++) { if (cams[i].name === name) { return cams[i]; } } } //===============动画组件================================ function YxAnimation() { var curr = null; var idx = -1; var playing = true; var animations = null; var raw = null; var clock = new THREE.Clock(); this.Awake = function () { raw = this.gameObject.threeObj; animations = this.gameObject.threeObj.animations raw.mixer = new THREE.AnimationMixer(raw); } this.Update = function () { if (curr != null && playing) { raw.mixer.update(clock.getDelta()); } } this.Play = function (name) { for (var i = 0; i < animations.length; i++) { if (animations[i].name === name) { var action = this.gameObject.threeObj.mixer.clipAction(animations[i]); if (curr != null) { curr.stop(); } curr = action; idx = i; playing = true; action.play(); } } } this.PlayIndex = function (i) { var action = this.gameObject.threeObj.mixer.clipAction(animations[i]); if (curr != null) { curr.stop(); } curr = action; idx = i; playing = true; action.play(); } this.GetCurrentClip = function () { return curr; } this.Pause = function () { playing = false; } this.Stop = function () { curr = null; playing = false; } this.IsPlayingIndex = function (i) { return idx === i && playing; } this.IsPlayingName = function (name) { return curr.name === name && playing } } //===============输入系统================================ // 码状态 0表示未按下,1表示按下 function KeyState(keycode) { this.key = keycode; this.preState = 0; this.state = 0 } function YxInput() { } YxInput.mouse = new THREE.Vector2(); var inputEvents = new Array(); var GetKeyState = function(key){ for (var i = 0; i < inputEvents.length; i++) { if (inputEvents[i].key === key) { return inputEvents[i]; } } return null; } var SetKeyState = function (key,state) { var keystate = GetKeyState(key); if (keystate == null) { keystate = new KeyState(key); inputEvents.push(keystate); } keystate.preState = keystate.state; keystate.state = state; return keystate; } YxInput.Init = function () { /// <summary> /// 输入系统的初始化,注册一些系统回调 /// </summary> document.onkeydown = function (e) { var keynum = window.event ? e.keyCode : e.which; var keychar = String.fromCharCode(keynum); var state = SetKeyState(keychar, 1); nextCallbacks.push(function () { state.preState = state.state; }); } document.onkeyup = function (e) { var keynum = window.event ? e.keyCode : e.which; var keychar = String.fromCharCode(keynum); var state = SetKeyState(keychar, 0); nextCallbacks.push(function () { state.preState = state.state; }); } document.onmousedown = function (e) { var idx = e.button; var state = SetKeyState('mouse' + idx, 1); nextCallbacks.push(function () { state.preState = state.state; }); } document.onmouseup = function (e) { var idx = e.button; var state = SetKeyState('mouse' + idx, 0); nextCallbacks.push(function () { state.preState = state.state; }); } document.onmousemove = function (e) { YxInput.mouse.set((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1); } ThreeECS.Instance.renderer.domElement.addEventListener('touchstart', function (e) { e.preventDefault(); var t0 = e.targetTouches[0]; YxInput.mouse.set((t0.pageX / window.innerWidth) * 2 - 1, -(t0.pageY / window.innerHeight) * 2 + 1); var idx = 0; var state = SetKeyState('mouse' + idx, 1); nextCallbacks.push(function () { state.preState = state.state; }); }); ThreeECS.Instance.renderer.domElement.addEventListener('touchend', function (e) { var t0 = e.changedTouches[0]; YxInput.mouse.set((t0.pageX / window.innerWidth) * 2 - 1, -(t0.pageY / window.innerHeight) * 2 + 1); var idx = 0; var state = SetKeyState('mouse' + idx, 0); nextCallbacks.push(function () { state.preState = state.state; }); }); } YxInput.GetKeyDown = function (key) { var state = GetKeyState(key); if (state == null) { return false; } else { return state.preState == 0 && state.state == 1; } } YxInput.GetKey = function(key){ var state = GetKeyState(key); if (state == null) { return false; } else { return state.preState == 1 && state.state == 1; } } YxInput.GetKeyUp = function (key) { var state = GetKeyState(key); if (state == null) { return false; } else { return state.preState == 1 && state.state == 0; } } YxInput.GetMouseButtonDown = function (index) { var state = GetKeyState('mouse'+index); if (state == null) { return false; } else { return state.preState == 0 && state.state == 1; } } YxInput.GetMouseButton = function (index) { var state = GetKeyState('mouse' + index); if (state == null) { return false; } else { return state.preState == 1 && state.state == 1; } } YxInput.GetMouseButtonUp = function (index) { var state = GetKeyState('mouse' + index); if (state == null) { return false; } else { return state.preState == 1 && state.state == 0; } } //================协程================================= function YxCoroutine() { /// <summary> /// 协程实现,类似于unity的意义的协程,没有采用ES6特性 /// </summary> var stack = new Array(); this.index = -1; this.AddTask = function (func) { /// <summary> /// 增加任务 /// </summary> /// <param name="func">待执行方法</param> stack.push(func); return this; }; this.Yield = function (y) { /// <summary> /// 增加一个中断 /// </summary> /// <param name="y">包含IsDone()方法的对象</param> stack.push(y); return this; }; this.Current = function () { if (this.index > stack.length - 1) { return null; } return stack[this.index]; }; this.MoveNext = function () { while (this.index < stack.length - 1) { this.index += 1; var curr = this.Current(); if (curr.hasOwnProperty('IsDone')) { return true; } curr(); } return false; }; this.Reset = function () { this.index = -1; }; this.IsCompleted = function () { return this.index >= stack.length - 1 }; this.Clear = function () { stack.length = 0; this.index = -1; } } function YxWaitForEndOfFrame() { /// <summary> /// 等待一帧 /// </summary> var startFrame = null; this.IsDone = function () { if (startFrame === null) { startFrame = YxTime.GetTotalFrame(); } return YxTime.GetTotalFrame() - startFrame > 0; } } function YxWaitForSeconds(sec) { /// <summary> /// 等待指定秒 /// </summary> /// <param name="sec">秒,float</param> var waitMS = sec; var timeStart = null; this.IsDone = function () { if (timeStart === null) { timeStart = YxTime.GetSecondsSinceStart(); } var r = YxTime.GetSecondsSinceStart() - timeStart > waitMS; return r; } } function YxCoroutineInvoker() { /// <summary> /// 协程驱动器 /// </summary> var cors = new Array(); this.StartCoroutine = function (cor) { /// <summary> /// 启动协程 /// </summary> if (!YxArray.Contain(cors, cor)) { cor.MoveNext(); cors.push(cor); } }; this.StopCoroutine = function (cor) { /// <summary> /// 停止协程 /// </summary> var idx = YxArray.IndexOf(cors, cor); if (idx >= 0) { cors.splice(idx,1); } }; this.Update = function () { for (var i = 0; i < cors.length; i++) { var cor = cors[i]; if (!cor.IsCompleted()) { var y = cor.Current(); while (y != null && y.IsDone() && cor.MoveNext()) { y = cor.Current(); } } } for (var i = cors.length - 1; i >= 0 ; i--) { if (cors[i].IsCompleted()) { cors.splice(i, 1); } } }; } var yxCoroutineInvoker = null; YxCoroutineInvoker.Instance = function () { if (yxCoroutineInvoker != null) { return yxCoroutineInvoker; } else { var invokerObj = new YxGameObject('Yx_Built-in_YxCoroutineInvoker', null); yxCoroutineInvoker = new YxCoroutineInvoker(); invokerObj.AddComponent(yxCoroutineInvoker); return yxCoroutineInvoker; } } YxCoroutineInvoker.StartCoroutine = function (cor) { YxCoroutineInvoker.Instance().StartCoroutine(cor); }; YxCoroutineInvoker.StopCoroutine = function (cor) { YxCoroutineInvoker.Instance().StopCoroutine(cor); }; //==========================时间=========================== function YxTime() { } var startTime = (new Date()).valueOf(); YxTime.GetSecondsSinceStart = function () { return ((new Date()).valueOf() - startTime) / 1000.00; } YxTime.frames = 0; YxTime.GetTotalFrame = function(){ return YxTime.frames; } //========================数组扩展========================== var YxArray = {}; YxArray.Contain = function (array, v) { for (var i = 0; i < array.length; i++) { if (array[i] === v) { return true; } } return false; } YxArray.IndexOf = function (array, v) { for (var i = 0; i < array.length; i++) { if (array[i] === v) { return i; } } return -1; } YxArray.Remove = function (array,v) { for (var i = 0; i < array.length; i++) { if (array[i] === v) { array.splice(i, 1); return true; } } return false; } //=========================================================
那,使用上就像下面这样:(js代码加入html标签里这些就不提了)main.js
//入口 function Main(){ // 初始化ECS var ECS = new ThreeECS(); ECS.AddToHtml(); var logic = new YxGameObject("logic", null); function logicObject() { // 新增相机 var rawCam = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 6000); var cam = new YxCamera("相机1", rawCam, 0); // 演示查找功能,找到相机并设置位置 var findCam = YxCamera.Find("相机1"); findCam.threeObj.position.set(0, 80, 30); // 演示组件系统,增加相机控制组件 cam.AddComponent(new CameraController(cam)); // 加一个平面 var g0 = new THREE.PlaneGeometry(10000, 10000, 1, 1); var m0 = new THREE.MeshLambertMaterial({ color: 0xffffff }); m0.map = THREE.ImageUtils.loadTexture("./tex/terrain.jpg"); // 设置平铺 g0.faceVertexUvs[0] = []; var t0 = [new THREE.Vector2(0, 100), new THREE.Vector2(0, 0), new THREE.Vector2(100, 100)]; var t1 = [new THREE.Vector2(0, 0), new THREE.Vector2(100, 0), new THREE.Vector2(100, 100)]; g0.faceVertexUvs[0] = [t0, t1]; m0.map.wrapS = THREE.RepeatWrapping; m0.map.wrapT = THREE.RepeatWrapping; // var yxg0 = new YxGameObject("terrain", new THREE.Mesh(g0, m0)); yxg0.threeObj.rotation.set(-0.5 * Math.PI, 0, 0); // 加一个cube var geometry = new THREE.CubeGeometry(5, 5, 5); var material = new THREE.MeshLambertMaterial({ color: 0xcc0033 }); var rawGO = new THREE.Mesh(geometry, material); var go = new YxGameObject("testModel", rawGO); go.threeObj.position.set(0, 3, 0); go.AddComponent(new Rotate()); // 演示输入组件,点鼠标中键变色 go.AddComponent(new RandomColor()); //go.SetActive(false); // 加一个环境光 var rawAmbient = new THREE.AmbientLight(0xa0a0a0); var ambient = new YxGameObject("环境光", rawAmbient); // 加一个方向光 var rawDL = new THREE.DirectionalLight(0x777777, 1) var DL = new YxGameObject("方向光", rawDL); // 演示查找功能,查找到方向光,并设置位置 var findDL = YxGameObject.Find("方向光"); findDL.threeObj.position.set(0, 0, 1); // 演示加载一个fbx var fbxloader = new THREE.FBXLoader(); fbxloader.load("./models/xsi_man_skinning.fbx", function (raw) { var fbx = new YxGameObject("fbx1", raw); fbx.threeObj.position.set(20, -10, 0); fbx.threeObj.scale.set(1, 1, 1); // 演示动画组件功能 var anim = fbx.AddComponent(new YxAnimation()); anim.Play("Take 001"); fbx.SetActive(true); // 演示逻辑 绕圈组件 fbx.AddComponent(new RotateAround(new THREE.Vector3(0, 0, 0), 20)); }, function () { console.log("progress");}, function (error) { console.log(error); }); // 演示加载天空盒 var path = "./tex/cube/sky1/";//设置路径 var directions = ["px", "nx", "py", "ny", "pz", "nz"];//获取对象 var format = ".png";//格式 var skyGeometry = new THREE.BoxGeometry(5000, 5000, 5000); var materialArray = []; for (var i = 0; i < 6; i++) materialArray.push(new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture(path + directions[i] + format), side: THREE.BackSide })); var skyMaterial = new THREE.MeshFaceMaterial(materialArray); var skyBox = new THREE.Mesh(skyGeometry, skyMaterial); new YxGameObject("天空盒", skyBox); // 演示,点击地面人跑过去 fbxloader.load("./models/xsi_man_skinning.fbx", function (raw) { var fbx2 = new YxGameObject("fbx2", raw); fbx2.threeObj.position.set(30, 0, 0); fbx2.threeObj.scale.set(1, 1, 1); var anim2 = fbx2.AddComponent(new YxAnimation()); anim2.Play("Take 001"); fbx2.AddComponent(new CharacterController()); }, function () { console.log("progress"); }, function (error) { console.log(error); }); }; logicInstacne = new logicObject(); logic.AddComponent(logicInstacne); } window.onload = Main;
其中,功能以组件的形式提供。
例如,一个点击变色功能:RandomColor.js
// 演示功能 // 按下鼠标中键,随机改变实体颜色 function RandomColor() { //var getRandomColor = function () { // return '0x' + // (function (color) { // return (color += '0123456789abcdef'[Math.floor(Math.random() * 16)]) // && (color.length == 6) ? color : arguments.callee(color); // })(''); //} this.Update = function () { if (YxInput.GetMouseButtonDown(1)) { var r = Math.random(); var g = Math.random(); var b = Math.random(); this.gameObject.threeObj.material.color.setRGB(r,g,b); } else { //console.log('没有按下a'); } } }
点击移动功能:CharacterController.js
// 点击地面,人走过去 function CharacterController() { var camera = YxCamera.Find("相机1").threeObj; var point; this.Start = function () { point = this.gameObject.threeObj.position; } this.Update = function () { if (YxInput.GetMouseButtonDown(0)) { var go = this.gameObject.threeObj; var x = YxInput.mouse.x; var y = YxInput.mouse.y; var vector = new THREE.Vector3(x, y, 0.5).unproject(camera); var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize()); var intersects = raycaster.intersectObjects(ThreeECS.Instance.scene.children,true); if (intersects.length > 0) { for (var i = 0; i < intersects.length; i++) { var o = intersects[i].object; if (o.name == "terrain") { point = intersects[i].point; break; } } } } var pos = this.gameObject.threeObj.position; if (pos.distanceTo(point) > 0.3) { var p = new THREE.Vector3(point.x, point.y, point.z); var dir = (p.sub(pos)).normalize(); var e = Math.asin(dir.x); if (dir.z<0) { e = Math.PI - e; } this.gameObject.threeObj.rotation.set(0, e, 0); pos.add(dir.multiplyScalar(0.4)); } } }
这些功能组件都是在main.js中加到对应的实体上面去的,可以参照上面的代码。
html比较简单:
<!DOCTYPE html> <html> <head> <title></title> <style> canvas { width: 100%; height: 100%; } </style> </head> <body> <script src="./com/three.js"></script> <script src="./com/FBXLoader.js"></script> <script src="./com/GLTFLoader.js"></script> <script src="./com/OrbitControls.js"></script> <script src="./com/ThreeECS.js"></script> <script src="./logic/import.js"></script> </body> </html>
import.js,把用到的js代码加入网页。
var array = new Array(); // logic array.push("./logic/CameraController.js"); array.push("./logic/Rotate.js"); array.push("./logic/RandomColor.js"); array.push("./logic/RotateAround.js"); array.push("./logic/CharacterController.js"); array.push("./logic/main.js"); for (var i = 0; i < array.length; i++) { new_element=document.createElement("script"); new_element.setAttribute("src",array[i]); document.body.appendChild(new_element); }
通过上面这些可以看到,通过这样的方式组织代码,一些低层次的细节在逻辑开发时就不需要关心了,比如场景管理,渲染,输入事件。当然,这个是仓促之作,也并不完善,仅代表一种思路。