网页小实验——在平面空间建立大量“可思考的”对象

  • A+
所属分类:Web前端
摘要

实验目标:建立大量对象(万级),为每个对象设置自身逻辑,并实现对象之间的交互,以原生DOM为渲染方式。主干在于对象逻辑,可根据需求换用其他渲染方式。

实验目标:建立大量对象(万级),为每个对象设置自身逻辑,并实现对象之间的交互,以原生DOM为渲染方式。主干在于对象逻辑,可根据需求换用其他渲染方式。

一、html舞台:

 1 <!DOCTYPE html>  2 <html lang="en">  3 <head>  4     <meta charset="UTF-8">  5     <title>比较整体重绘和移动重绘的效率</title>  6     <link href="../../CSS/newland.css" rel="stylesheet">  7     <style>  8         div{position: absolute}  9         #div_allbase,#div_map{background-color: darkseagreen} 10         .div_unit{background-repeat: no-repeat;background-size: 100% 100%} 11     </style> 12     <script src="../../JS/MYLIB/newland.js"></script> 13     <script src="./ais.js"></script> 14     <script src="./dos.js"></script> 15     <script src="./commands.js"></script> 16     <script src="./vectorTools.js"></script> 17 </head> 18 <body> 19 <div id="div_allbase" style="width: 100%;height: 100%"> 20     <div id="div_map" ></div> 21     <div id="fps" style="z-index: 302;position:fixed"></div> 22     <div id="div_console" style="z-index: 302;position: fixed;height: 60px;width: 100%;bottom:0px;background-color: #cccccc"> 23         <div id="div_minimap" style="position:fixed;height:60px;width: 80px;left:0px;bottom:0px;background-color: darkseagreen;overflow: hidden"> 24             <div id="div_miniview" style="position:absolute;background-color: #ffffff"></div> 25         </div> 26         <button class="btn_console" onclick="changeView('上')"></button> 27         <button class="btn_console" onclick="changeView('下')"></button> 28         <button class="btn_console" onclick="changeView('左')"></button> 29         <button class="btn_console" onclick="changeView('右')"></button> 30         <button class="btn_console" onclick="changeView('放大')">放大</button> 31         <button class="btn_console" onclick="changeView('缩小')">缩小</button> 32         <br> 33         <button class="btn_console" onclick="runOneStep()">步进</button> 34         <button class="btn_console" onclick="runLoop()">自动</button> 35         <button class="btn_console" onclick="stopRunLoop()">暂停</button> 36     </div> 37  38 </div> 39 </body> 40 <script> 41 //主体程序入口 42 </script> 43 </html>

 

其中“div_map”是大量单位的绘制区域,“fps”是显示帧率的标签没有使用,“div_console”是位于屏幕底部的控制区域,其中包括一些控制场景运行的按钮和一个显示窗口位置的迷你地图。

二、场景初始化

1、初始化前的准备:

 1  window.onload=beforeInit;  2     function beforeInit()  3     {  4         //这里可以做一些运行环境检测和渲染方式选择操作  5         Init();  6     }  7     var obj_units={};//用id索引所有单位  8     var obj_unitclassed={};//用class索引所有单位类型  9     var arr_part=[];//多重数组,地图分块,优化单位查找速度,最下层是数组 10     var arr_partowner=[];//多重数组,用来分块优化势力占据,最下层是对象 11     var obj_owners={ 12         red:{color:"red",name:"red",arr_unit:[],countAlive:0}, 13         blue:{color:"blue",name:"blue",arr_unit:[],countAlive:0} 14     }//所有势力 15     var div_allbase,div_map; 16     var mapWidth=4096*2,mapHeight=3072*2,partSizeX=200,partSizeY=200;//地图宽度、地图高度、每个地图分块的尺寸 17     var flag_runningstate="beforeStart";//系统总体运行状态(未用到) 18     var flag_autorun=false;//是否自动运行,默认非自动运行,点击一次“步进”按钮计算一次。 19     function Init() 20     { 21       //初始化代码       22     }

其中arr_part和arr_partowner用来把地图分成多块,这样一个单位在寻找其他单位时就可以从临近的小块找起,不必遍历地图上的所有单位,把这样的数组称为“索引数组”。

2、初始化地图

 1 //InitMap  2         div_allbase=document.getElementById("div_allbase");  3         div_map=document.getElementById("div_map");  4         div_map.style.width=mapWidth+"px";  5         div_map.style.height=mapHeight+"px";  6         var partCountX=Math.ceil(mapWidth/partSizeX);  7         var partCountY=Math.ceil(mapHeight/partSizeY);  8         for(var i=0;i<partCountX;i++)//为地图上的每个区域分配一个数组元素  9         { 10             var arr=[]; 11             for(var j=0;j<partCountY;j++) 12             { 13                 arr.push([]); 14             } 15             arr_part.push(arr); 16  17             var arr=[]; 18             for(var j=0;j<partCountY;j++) 19             { 20                 arr.push({}); 21             } 22             arr_partowner.push(arr); 23         }

3、初始化控制按钮

 1 //InitUI  2         var arr_btn=document.getElementsByClassName("btn_console");  3         arr_btn[0].onclick=function(){changeView('上')};  4         arr_btn[1].onclick=function(){changeView('下')};  5         arr_btn[2].onclick=function(){changeView('左')};  6         arr_btn[3].onclick=function(){changeView('右')};  7         arr_btn[4].onclick=function(){changeView('放大')};  8         arr_btn[5].onclick=function(){changeView('缩小')};  9         var div_miniview=document.getElementById("div_miniview"); 10         div_miniview.style.width=80*(window.innerWidth/mapWidth)/Math.pow(2,zoomRate)+"px"; 11         div_miniview.style.height=60*(window.innerHeight/mapHeight)/Math.pow(2,zoomRate)+"px"; 12         window.onresize=function(){ 13             var div_miniview=document.getElementById("div_miniview"); 14             div_miniview.style.width=80*(window.innerWidth/mapWidth)/Math.pow(2,zoomRate)+"px"; 15             div_miniview.style.height=60*(window.innerHeight/mapHeight)/Math.pow(2,zoomRate)+"px"; 16         }

其中“zoomRate”是地图的缩放比例,“div_miniview”在迷你地图里表示可视区域,当缩放比例放大时,单位变大,窗口中可见的单位变少,迷你地图的可视区域变小。

4、初始化单位类型:

 1 //InitUnitCalss  2         var Yt=function(p){//构造野兔类,并且为它设置一些属性  3             this.className="Yt";  4             this.hp=10;  5             this.mp=0;  6             this.sp=10;  7             this.at=2;  8             this.df=1;  9             this.clipSize=20;//碰撞尺寸 10             this.nearAttRange=20;//近战攻击范围 11             this.pic="./yetu.jpg"; 12             this.obj_skills={};//技能列表 13             this.left=p.left;//位置 14             this.top=p.top; 15             this.id=p.id; 16             this.owner=p.owner;//所属势力 17             this.doing="standing";//正在做的事 18             this.wanting="waiting";//想要做的事 19             this.being="free";//正在遭受 20             this.speed=50;//移动速度 21             this.view=500;//视野 22         } 23         Yt.prototype.render=function(){//渲染方法 24             if(this.doing=="dead") 25             { 26                 return "<div class='div_unit' " + 27                     "style='background-color:black;" + 28                     "width:"+this.clipSize+"px ;height:"+this.clipSize+"px;left:"+this.left+"px;top:"+this.top+"px;" + 29                     "border:2px solid "+this.owner.color+"'>" + 30                     "</div>" 31             } 32             if(zoomRate>=2) 33             { 34                 return "<div class='div_unit' " + 35                     "style='background-image: url("+this.pic+");" + 36                     "width:"+this.clipSize+"px ;height:"+this.clipSize+"px;left:"+this.left+"px;top:"+this.top+"px;" + 37                     "border:2px solid "+this.owner.color+"'>" + 38                         "<div style='background: inherit;width: 100%;height:100%;zoom: 0.1;font-size: 12px;overflow: hidden;color:"+this.owner.color+"'>" + 39                             "<div style='top:20px'>doing:"+this.doing+"</div>" + 40                             "<div style='top:40px'>wanting:"+this.wanting+"</div>" + 41                             "<div style='top:60px'>being:"+this.being+"</div>" + 42                     "</div>" + 43                     "</div>" 44             } 45             else if(zoomRate>=-1) 46             { 47                 return "<div class='div_unit' " + 48                     "style='background-image: url("+this.pic+");" + 49                     "width:"+this.clipSize+"px ;height:"+this.clipSize+"px;left:"+this.left+"px;top:"+this.top+"px;" + 50                     "border:2px solid "+this.owner.color+"'>" + 51                     "</div>" 52             } 53             else 54             { 55                 return "<div class='div_unit' " + 56                     "style='" + 57                     "width:"+this.clipSize+"px ;height:"+this.clipSize+"px;left:"+this.left+"px;top:"+this.top+"px;" + 58                     "border:2px solid "+this.owner.color+"'>" + 59                     "</div>" 60             } 61  62         } 63         Yt.prototype.think=obj_ais.近战战士;//思考方式 64         Yt.prototype.do=obj_dos.DOM;//渲染方式,2d网页标签的渲染 65         obj_unitclassed.Yt=Yt;

这里根据当前缩放比例的不同为野兔设置了不同的渲染方式,缩放比例大时显示更多细节。其中“pic”属性是代表野兔的图片:

网页小实验——在平面空间建立大量“可思考的”对象

5、初始化单位:

 1 //InitUnit  2         for(var i=0;i<10000;i++)//随机建立10000只野兔  3         {  4             var obj_yt=new obj_unitclassed.Yt({left:newland.RandomBetween(500,mapWidth-500),top:newland.RandomBetween(500,mapHeight-500)  5                 ,id:"yt_"+i,owner:newland.RandomChooseFromObj(obj_owners)});  6             obj_units[obj_yt.id]=obj_yt;  7             var arr_unit=obj_owners[obj_yt.owner.name].arr_unit;  8             obj_yt.ownerNumber=arr_unit.length;  9             arr_unit.push(obj_yt); 10         } 11         obj_owners.red.countAlive=obj_owners.red.arr_unit.length; 12         obj_owners.blue.countAlive=obj_owners.blue.arr_unit.length; 13         列方阵("blue",{left:2000,top:4000},Math.PI/6,100,20,obj_owners); 14         列方阵("red",{left:6000,top:4000},Math.PI*7/6,100,20,obj_owners); 15         for(var key in obj_units) 16         { 17             var obj_yt=obj_units[key]; 18             obj_yt.left=obj_yt.target[0].left; 19             obj_yt.top=obj_yt.target[0].top; 20             obj_yt.target=[]; 21             obj_yt.wanting="waiting"; 22  23             obj_yt.x=Math.floor(obj_yt.left/partSizeX); 24             obj_yt.y=Math.floor(obj_yt.top/partSizeY); 25             arr_part[obj_yt.x][obj_yt.y].push(obj_yt); 26             var obj_temp=arr_partowner[obj_yt.x][obj_yt.y]; 27             if(!obj_temp[obj_yt.owner.name]) 28             { 29                 obj_temp[obj_yt.owner.name]=1; 30             } 31             else 32             { 33                 obj_temp[obj_yt.owner.name]++; 34             } 35         }

其中“列方阵”方法是写在command.js中的一个“命令方法”,可以在程序运行时执行这些命令方法改变单位的wanting属性,进而引导单位接下来的行为,command.js内容如下:

 1 function 列方阵(ownerName,pos,angle,col,dis,obj_owners)//势力名、方阵左上角位置、方阵从right方向起顺时针旋转角度、方阵每行最大人数(也就是最大列数)、单位间距、势力列表  2 {  3     var arr_unit=obj_owners[ownerName].arr_unit;  4     var len=arr_unit.length;  5     for(var i=0;i<len;i++)  6     {  7         var unit=arr_unit[i];  8         if(unit.doing!="dead"&&unit.doing!="unconscious")  9         { 10             unit.wanting="lineup";//单位想要去排队 11             var left0=(i%col)*dis; 12             var top0=Math.floor(i/col)*dis; 13             var r0=Math.pow(left0*left0+top0*top0,0.5); 14             var angle0 15             if(left0==0) 16             { 17                 angle0=Math.PI/2; 18             } 19             else 20             { 21                 angle0=Math.atan(top0/left0); 22             } 23             var angle1=angle0+angle; 24             var left1=Math.max(pos.left+r0*Math.cos(angle1),0); 25             var top1=Math.max(pos.top+r0*Math.sin(angle1)); 26             unit.target=[{left:left1,top:top1}];//单位想要去这里 27         } 28     } 29 } 30 function 自由冲锋(ownerName,obj_owners,targetName){//为每个单位选择单独的目标最少需要消耗十几毫秒(加trycatch的情况下),去掉trycatch速度提升百倍 31     var arr_unit=obj_owners[ownerName].arr_unit; 32     var len=arr_unit.length; 33     for(var i=0;i<len;i++) 34     { 35         var unit=arr_unit[i]; 36         if(unit.doing!="dead"&&unit.doing!="unconscious") 37         { 38             unit.wanting="freecharge"; 39             unit.target=[targetName] 40         } 41     } 42 }

可以看到“列方阵”方法为势力中的所有对象分配了一个方阵位置,然后初始化方法直接把target设为单位当前位置,这样10000只野兔刚出场时就是排号方阵的。

接下来计算每个单位属于arr_part和arr_partowner的哪个小块。

6、初始化渲染和渲染循环:

1 //TestRender 2         div_map.style.zoom=Math.pow(2,zoomRate)+'';//对单位显示区应用地图缩放 3         renderMap(); 4         自由冲锋("blue",obj_owners,"red"); 5         自由冲锋("red",obj_owners,"blue"); 6  7         //InitRenderLoop 8         Loop();

其中renderMap方法用来绘制单位显示区:

function renderMap(){         //绘制整个地图,考虑到运行效率,只render当前显示范围内的!!!!,那么移动时也要重新绘制!!??         var innerWidth=window.innerWidth;         var innerHeight=window.innerHeight;         var left=parseInt(div_map.style.left||0);         var top=parseInt(div_map.style.top||0);         var y1=(-top);///Math.pow(2,zoomRate);//画面缩小时,同样的地图位移意味着更多的距离         var x1=(-left);///Math.pow(2,zoomRate);         var y2=y1+innerHeight/Math.pow(2,zoomRate);         var x2=x1+innerWidth/Math.pow(2,zoomRate);//以上找到了可视区域         var arr_temp=[];         for(var key in obj_units)         {             var unit=obj_units[key];             if(unit.left>(x1-unit.clipSize)&&unit.left<x2&&unit.top>(y1-unit.clipSize)&&unit.top<y2)             {//绘制可视区域内的单位                 arr_temp.push(obj_units[key].render());             }          }         div_map.innerHTML=arr_temp.join("");//一次性绘制     }

绘制显示区有两种思路,一是一次性绘制显示区中的所有单位,这样移动视口时就不必重新绘制单位;而是只绘制显示在显示区域内的单位,视口改变时临时绘制新显示出的单位,经过实验,在当前配置下第二种方式的绘制速度远大于第一种。

绘制代码的最后,把每个单位的render方法返回的html文本合并起来一次性绘制,这样做的绘制速度远远高于分别绘制每一单位(数百倍到上千倍),其原理在于每条修改innerHTML或createElement的操作都会触发一次js引擎到浏览器渲染引擎的通信,之后浏览器渲染引擎将对页面进行绘制,而js引擎将挂起等待绘制完成,并且此过程中的js挂起时间远大于js计算和通信时间,一次性绘制则可以节省掉大量的页面绘制次数,能够大大提升渲染速度。

顺便记录这种一次性绘制与React“虚拟DOM”的关系——事实上React的底层绘制仍然是使用createElement建立每个标签,并没有实现一次性绘制,而虚拟DOM的特点在于“把所有createElement集中起来一起执行,在统一执行前,合并对同一标签的反复修改并检查虚拟DOM有无变化,如虚拟DOM无变化则不执行”,以此减少绘制次数。

可以用谷歌浏览器的performance功能配合以下程序测试这一区别:(以下内容缺乏大量充分试验,并且与本文主干无关)

组件:

网页小实验——在平面空间建立大量“可思考的”对象网页小实验——在平面空间建立大量“可思考的”对象

 1 import { Component, Fragment } from "react";  2   3 //测试setState和传统dom操作的性能差距  4 class App extends Component {  5     constructor(props){  6         super();  7         this.state={  8             arr:[],  9         } 10  11     } 12     onClick1=()=>{ 13         console.log(new Date().getTime()) 14         var arr=[]; 15         for(var i=0;i<10000;i++) 16         { 17             arr.push({key:i}); 18         } 19         this.setState({arr:arr}); 20     } 21     componentDidUpdate() 22     { 23         console.log(new Date().getTime()) 24     } 25     onClick2=()=>{ 26         console.log(new Date().getTime()); 27         var div_allbase=document.getElementById("div_allbase"); 28         for(var i=0;i<10000;i++) 29         { 30             var div=document.createElement("div") 31             div.innerHTML=i; 32             div_allbase.appendChild(div); 33         } 34         console.log(new Date().getTime()); 35     } 36     onClick3=()=>{ 37         console.log(new Date().getTime()); 38         var div_allbase=document.getElementById("div_allbase"); 39         var div0=document.createElement("div") 40         for(var i=0;i<10000;i++) 41         { 42             var div=document.createElement("div")//这一步还是有多余的js引擎到dom引擎的通信 43             div.innerHTML=i; 44             div0.appendChild(div); 45         } 46         div_allbase.appendChild(div0); 47         console.log(new Date().getTime()); 48     } 49     onClick4=()=>{ 50         console.log(new Date().getTime()); 51         var div_allbase=document.getElementById("div_allbase"); 52         var str=""; 53         var arr=[]; 54         for(var i=0;i<10000;i++) 55         { 56             arr.push("<div>"+i+"</div>"); 57         } 58         str=arr.join(""); 59         div_allbase.innerHTML=str; 60         console.log(new Date().getTime()); 61     } 62     render() { 63         const { count, price,show } = this.state; 64         return <Fragment> 65             <button onClick={()=>this.onClick1()}>setState方法</button> 66             <button onClick={()=>this.onClick2()}>Dom方法</button> 67             <button onClick={()=>this.onClick3()}>一次添加方法</button> 68             <button onClick={()=>this.onClick4()}>innerHTML方法</button> 69             <div id={"div_allbase"} style={{}}> 70                 {this.state.arr.map(d=><div key={d.key}>{d.key}</div>) } 71              </div> 72         </Fragment> 73     } 74 } 75  76 export default App;

View Code

index.js

网页小实验——在平面空间建立大量“可思考的”对象网页小实验——在平面空间建立大量“可思考的”对象

 1 import React from 'react';  2 import ReactDOM from 'react-dom';  3 import './index.css';  4 import App from './test2/App4';  5   6 ReactDOM.render(  7   <React.StrictMode>  8     <App />  9   </React.StrictMode>, 10   document.getElementById('root') 11 );

View Code

package.json:

网页小实验——在平面空间建立大量“可思考的”对象网页小实验——在平面空间建立大量“可思考的”对象

 1 {  2   "name": "my-app4",  3   "version": "0.1.0",  4   "private": true,  5   "dependencies": {  6     "@testing-library/jest-dom": "^5.11.4",  7     "@testing-library/react": "^11.1.0",  8     "@testing-library/user-event": "^12.1.10",  9     "antd": "^4.9.2", 10     "babylonjs": "^4.2.0", 11     "codeflask": "^1.4.1", 12     "moment": "^2.29.1", 13     "react": "^17.0.1", 14     "react-dom": "^17.0.1", 15     "react-router-dom": "^5.2.0", 16     "react-scripts": "4.0.1", 17     "web-vitals": "^0.2.4" 18   }, 19   "scripts": { 20     "start": "react-scripts start", 21     "build": "react-scripts build", 22     "test": "react-scripts test", 23     "eject": "react-scripts eject" 24   }, 25   "eslintConfig": { 26     "extends": [ 27       "react-app", 28       "react-app/jest" 29     ] 30   }, 31   "browserslist": { 32     "production": [ 33       ">0.2%", 34       "not dead", 35       "not op_mini all" 36     ], 37     "development": [ 38       "last 1 chrome version", 39       "last 1 firefox version", 40       "last 1 safari version" 41     ] 42   } 43 }

View Code

结果,单纯从一次渲染大量标签的速度来看React的setState<每个标签单独appendChild<先将大量标签放在一个容器中,然后把容器append到body<使用innerHTML一次性修改。

以下是分别渲染100000个div时的耗时情况:

网页小实验——在平面空间建立大量“可思考的”对象

 

 

 7、视口移动和缩放

网页小实验——在平面空间建立大量“可思考的”对象网页小实验——在平面空间建立大量“可思考的”对象

 1 var sizeStep=250;  2     var zoomRate=-3;  3     var scrollTop=0,scrollLeft=0;  4     function changeView(type){  5         var div_miniview=document.getElementById("div_miniview");  6         if(type=="上")  7         {  8             scrollTop=scrollTop+sizeStep/Math.pow(2,zoomRate);//画面放大时,卷屏更慢  9             if(scrollTop>0) 10             { 11                 scrollTop=0; 12  13             } 14             div_map.style.top=scrollTop+"px"; 15             renderMap(); 16             div_miniview.style.top=-60*(scrollTop/mapHeight)+"px"; 17         } 18         else if(type=="下") 19         { 20             scrollTop=scrollTop-sizeStep/Math.pow(2,zoomRate); 21             if(scrollTop<-mapHeight) 22             { 23                 scrollTop=-mapHeight; 24  25             } 26             div_map.style.top=scrollTop+"px"; 27             renderMap(); 28             div_miniview.style.top=-60*(scrollTop/mapHeight)+"px"; 29         } 30         else if(type=="左") 31         { 32             scrollLeft=scrollLeft+sizeStep/Math.pow(2,zoomRate); 33             if(scrollLeft>0) 34             { 35                 scrollLeft=0; 36             } 37             div_map.style.left=scrollLeft+"px"; 38             renderMap(); 39             div_miniview.style.left=-80*(scrollLeft/mapWidth)+"px"; 40         } 41         else if(type=="右") 42         { 43             scrollLeft=scrollLeft-sizeStep/Math.pow(2,zoomRate); 44             if(scrollLeft<-mapWidth) 45             { 46                 scrollLeft=-mapWidth; 47             } 48             div_map.style.left=scrollLeft+"px"; 49             renderMap(); 50             div_miniview.style.left=-80*(scrollLeft/mapWidth)+"px"; 51         } 52         else if(type=="放大") 53         { 54             zoomRate++; 55             if(zoomRate>3) 56             { 57                 zoomRate=3; 58             } 59  60             div_map.style.zoom=Math.pow(2,zoomRate); 61             renderMap(); 62             //zoomRate=zoomRate*2; 63             //div_map.style.zoom=zoomRate; 64             div_miniview.style.width=80*(window.innerWidth/mapWidth)/Math.pow(2,zoomRate)+"px"; 65             div_miniview.style.height=60*(window.innerHeight/mapHeight)/Math.pow(2,zoomRate)+"px"; 66         } 67         else if(type=="缩小") 68         { 69             zoomRate--; 70             if(zoomRate<-3) 71             { 72                 zoomRate=-3; 73             } 74             div_map.style.zoom=Math.pow(2,zoomRate); 75             renderMap(); 76             div_miniview.style.width=80*(window.innerWidth/mapWidth)/Math.pow(2,zoomRate)+"px"; 77             div_miniview.style.height=60*(window.innerHeight/mapHeight)/Math.pow(2,zoomRate)+"px"; 78             // zoomRate=zoomRate/2; 79             // div_map.style.zoom=zoomRate; 80         } 81     }

View Code

主要是html标签的样式操作。

8、渲染循环

 1 function runOneStep(){//遍历每个unit并决定它要做的事,runLoop也要调用这个方法,暂时把思考、行动、渲染放在同步的帧里,思考频率、移动速度、单位大小要相互和谐,以正常移动避免碰撞为标准  2         for(var key in obj_units)//思考  3         {  4             var unit=obj_units[key];  5             if(unit.doing!="dead"&&unit.doing!="unconscious")  6             {  7                 unit.think(unit,obj_units,arr_part);  8             }  9         } 10         for(var key in obj_units)//行动 11         { 12             var unit=obj_units[key]; 13             if(unit.doing!="dead"&&unit.doing!="unconscious") 14             { 15                 unit.do(unit); 16             } 17         } 18         renderMap();//渲染 19     } 20     function runLoop(){ 21         flag_autorun=true; 22     } 23     function stopRunLoop(){ 24         flag_autorun=false; 25     } 26     var lastframe=new Date().getTime(); 27     function Loop() 28     { 29         if(flag_autorun) 30         { 31             runOneStep(); 32             var thisframe=new Date().getTime(); 33             console.log(thisframe-lastframe,"red:"+obj_owners.red.countAlive,"blue:"+obj_owners.blue.countAlive); 34             lastframe=thisframe; 35         } 36         window.requestAnimationFrame(function(){Loop()}); 37     }

事实上可思考的对象存在三个循环“思考循环”——比如最小间隔1秒考虑一次接下来做什么,“行动循环”——比如受到持续伤害时最小间隔0.2秒修改生命值,“渲染循环”——比如播放60帧每秒的模型动画,这里为了省事把三个循环合在一起执行。

三、思考

ais.js:

 1 var obj_ais={};//ai列表  2 obj_ais.近战战士=function(unit,obj_units,arr_part){//通过原型方法调用,那么这里的this应该是谁??  3     if(unit.wanting=="lineup")//如果单位现在想列队,排队也有两种思路,一是靠临近人员自组织,二是获取所有单位统筹规划  4     {//这时应该有一个target参数指明单位要列队的地点,这个地点应该是队形中心还是单位的实际点??  5         //应该在列队开始时就为每个单位设置精确目标点,还是在运动中实时缩小范围?还是每个单位都在创建时就分配一个队伍位置??  6         var pos_target=unit.target[0];  7         if(unit2isat(unit,pos_target))  8         {//如果单位已经到位  9             unit.wanting="waiting"; 10             unit.doing="standing"; 11             unit.target=[]; 12         } 13         else 14         { 15             unit.doing="walk"; 16             if(unit2distance(unit,pos_target)<unit.speed)//如果目标已经在一次思考时间的移动范围内 17             { 18                 unit.nextpos=pos_target; 19             } 20             else 21             {//计算下一步的位置 22                 unit.nextpos=unit2add(unit2times(unit2normal(unit2substract(unit,pos_target)),unit.speed),unit); 23             } 24         } 25  26  27  28  29     } 30     if(unit.wanting=="freecharge")      //冲锋过程中的每次思考都要重新规划目标 31     { 32         if(!unit.aimAt||unit.aimAt.doing=="dead")//如果还没有瞄准的目标,或者瞄准的目标已经死亡,则要寻找一个瞄准目标 33         { 34             var arr_res=findNearUnit(arr_part,0,20,"nearest-target-notdead-onlyone",unit.x,unit.y,unit,arr_partowner);//找到了一个目标 35             unit.aimAt=arr_res[0]; 36         } 37         if(unit.aimAt) 38         { 39             var dis=unit2distance(unit,unit.aimAt); 40             if(dis<unit.nearAttRange)//如果进入射程 41             { 42                 unit.doing="nearattack"//近战普通攻击 43             } 44             else 45             { 46  47                 if(dis<unit.speed)//如果目标已经在一次思考时间的移动范围内,移动要留下攻击目标的半径 48                 { 49                     unit.doing="chargeattack";//冲锋攻击 50                     unit.nextpos=unit2substract(unit2times(unit2normal(unit2substract(unit,unit.aimAt)),unit.aimAt.clipSize/2),unit.aimAt,); 51                 } 52                 else 53                 { 54                     unit.doing="walk"; 55                     unit.nextpos=unit2add(unit2times(unit2normal(unit2substract(unit,unit.aimAt)),unit.speed),unit); 56                 } 57             } 58         } 59         else 60         { 61             unit.doing="standing";//如果已经找不到敌人则恢复静默状态 62         } 63     } 64 }

这里建立了一个叫做“近战战士”的思考方法,接着根据单位wanting属性的不同为他设置不同的doing、target、nextpos等属性。

四、行动

dos.js

 1 var obj_dos={}  2 obj_dos.DOM=function(unit){  3     if(unit.doing=="walk")  4     {  5         unit.left=Math.max(unit.nextpos.left,0);  6         unit.top=Math.max(unit.nextpos.top,0);  7   8         var x=Math.floor(unit.left/partSizeX);  9         var y=Math.floor(unit.top/partSizeY); 10         if(x!=unit.x||y!=unit.y)//更新索引位置 11         { 12             unit.x=x; 13             unit.y=y; 14             updatePart(unit,arr_part,arr_partowner); 15         } 16     } 17     else if(unit.doing=="nearattack") 18     { 19         var unitAimAt=unit.aimAt; 20         if(unitAimAt.doing!="dead") 21         { 22             if(unitAimAt.being=="free") 23             { 24                 unitAimAt.being="hp-"+unit.at; 25  26             } 27             else 28             { 29                 unitAimAt.being+=";hp-"+unit.at; 30             } 31             unitAimAt.hp-=unit.at; 32             if(unitAimAt.hp<1) 33             { 34                 unitAimAt.doing="dead"; 35                 obj_owners[unitAimAt.owner.name].countAlive--; 36                 arr_partowner[unitAimAt.x][unitAimAt.y][unitAimAt.owner.name]--; 37             } 38         } 39     } 40     else if(unit.doing=="chargeattack") 41     { 42         unit.left=Math.max(unit.nextpos.left,0); 43         unit.top=Math.max(unit.nextpos.top,0); 44         var x=Math.floor(unit.left/partSizeX); 45         var y=Math.floor(unit.top/partSizeY); 46         if(x!=unit.x||y!=unit.y)//更新索引位置 47         { 48             unit.x=x; 49             unit.y=y; 50             updatePart(unit,arr_part,arr_partowner); 51         } 52  53         var unitAimAt=unit.aimAt; 54         if(unitAimAt.doing!="dead") 55         { 56             if(unitAimAt.being=="free") 57             { 58                 unitAimAt.being="hp-"+unit.at; 59  60             } 61             else 62             { 63                 unitAimAt.being+=";hp-"+unit.at; 64             } 65             unitAimAt.hp-=unit.at; 66             if(unitAimAt.hp<1) 67             { 68                 unitAimAt.doing="dead"; 69                 obj_owners[unitAimAt.owner.name].countAlive--; 70                 arr_partowner[unitAimAt.x][unitAimAt.y][unitAimAt.owner.name]--; 71             } 72         } 73     } 74 }

根据单位doing属性的不同,修改单位自身或其他单位的属性。

五、平面向量计算与快速单位查找

vectorTools.js:

  1 function findNearUnit(arr_part,start,count,type,x,y,unit,arr_partowner)   2 {//以自身为出发点,寻找附近的单位   3     var arr_res=[],arr_find1,arr_find2,arr_find3,arr_find4;   4     if(type=="all")//遍历所有格找寻所有符合条件的格   5     {   6         for (var i = start; i <= count; i++)//x.y相加的总步数,0就是本格   7         {   8             for (var xStep1 = 0; xStep1 <= i; xStep1++)   9             {  10                 var yStep1 = i - Math.abs(xStep1);  11                 //var yStep2 = -yStep1;  12                 //var xStep2 = -xStep1;  13                 //这里要注意处理数组为null的情况  14                 var arr2 = arr_part[x + xStep1];//使用arr_part查找附近单位,避免遍历所有单位  15                 arr_find1 = arr2 ? (arr2[y + yStep1]) : null;  16                 arr_find2 = arr2 ? (arr2[y - yStep1]) : null;  17                 arr2 = arr_part[x - xStep1];  18                 arr_find3 = arr2 ? (arr2[y + yStep1]) : null;  19                 arr_find4 = arr2 ? (arr2[y - yStep1]) : null;  20                 if(arr_find1)  21                 {  22                     arr_res=arr_res.concat(arr_find1);  23                 }  24                 if(arr_find2)  25                 {  26                     arr_res=arr_res.concat(arr_find2);  27                 }  28                 if(arr_find3)  29                 {  30                     arr_res=arr_res.concat(arr_find3);  31                 }  32                 if(arr_find4)  33                 {  34                     arr_res=arr_res.concat(arr_find4);  35                 }  36             }  37         }  38   39     }  40     else if(type=="nearest"){//从近向远遍历,取并列最近的所有格,之后停止遍历  41         for (var i = start; i <= count; i++)//x.y相加的总步数,0就是本格  42         {  43             for (var xStep1 = 0; xStep1 <= i; xStep1++) {  44                 var yStep1 = i - Math.abs(xStep1);  45                 var yStep2 = -yStep1;  46                 var xStep2 = -xStep1;  47                 //这里要注意处理数组为null的情况  48                 var arr2 = arr_part[x + xStep1];  49                 arr_find1 = arr2 ? (arr2[y + yStep1]) : null;  50                 arr_find2 = arr2 ? (arr2[y - yStep1]) : null;  51                 arr2 = arr_part[x - xStep1];  52                 arr_find3 = arr2 ? (arr2[y + yStep1]) : null;  53                 arr_find4 = arr2 ? (arr2[y - yStep1]) : null;  54                 if(arr_find1)  55                 {  56                     arr_res=arr_res.concat(arr_find1);  57                 }  58                 if(arr_find2)  59                 {  60                     arr_res=arr_res.concat(arr_find2);  61                 }  62                 if(arr_find3)  63                 {  64                     arr_res=arr_res.concat(arr_find3);  65                 }  66                 if(arr_find4)  67                 {  68                     arr_res=arr_res.concat(arr_find4);  69                 }  70             }  71             if(arr_res.length>0)  72             {  73                 break;  74             }  75         }  76     }  77     else if(type=="nearest-target-notdead-onlyone")  78     {//取最近的、属于规定势力的、没有死亡的、只取一个  79         var arr_res0=[];  80         var owner_target=unit.target[0];  81         for (var i = start; i <= count; i++)//x.y相加的总步数,0就是本格  82         {  83             for (var xStep1 = 0; xStep1 <= i; xStep1++) {  84                 var yStep1 = i - Math.abs(xStep1);  85                 var yStep2 = -yStep1;  86                 var xStep2 = -xStep1;  87                 //这里要注意处理数组为null的情况  88                 //四个方向随机选择  89                 //这里要避免单位不断遍历身边的大量己方单位!!  90                 //try catch对性能有额外消耗??!!  91                 var count2=0;  92                 // try{  93                 //     count2+=arr_partowner[x + xStep1][y + yStep1][owner_target]||0;  94                 // }catch(e){}  95                 // try{  96                 //     count2+=arr_partowner[x + xStep1][y - yStep1][owner_target]||0;  97                 // }catch(e){}  98                 // try{  99                 //     count2+=arr_partowner[x - xStep1][y + yStep1][owner_target]||0; 100                 // }catch(e){} 101                 // try{ 102                 //     count2+=arr_partowner[x - xStep1][y - yStep1][owner_target]||0; 103                 // }catch(e){} 104                 var arr2=arr_partowner[x + xStep1]; 105                 if(arr2) 106                 { 107                     var obj=arr2[y + yStep1]; 108                     if(obj) 109                     { 110                         count2+=obj[owner_target]||0; 111                     } 112                     var obj=arr2[y - yStep1]; 113                     if(obj) 114                     { 115                         count2+=obj[owner_target]||0; 116                     } 117                 } 118                 var arr2=arr_partowner[x - xStep1]; 119                 if(arr2) 120                 { 121                     var obj=arr2[y + yStep1]; 122                     if(obj) 123                     { 124                         count2+=obj[owner_target]||0; 125                     } 126                     var obj=arr2[y - yStep1]; 127                     if(obj) 128                     { 129                         count2+=obj[owner_target]||0; 130                     } 131                 } 132                 if(count2>0)//如果找到了对应目标的领域,则认为一定能找到一个目标??(活的)!! 133                 { 134                     var arr2 = arr_part[x + xStep1]; 135                     arr_find1 = arr2 ? (arr2[y + yStep1]) : []; 136                     arr_find2 = arr2 ? (arr2[y - yStep1]) : []; 137                     arr2 = arr_part[x - xStep1]; 138                     arr_find3 = arr2 ? (arr2[y + yStep1]) : []; 139                     arr_find4 = arr2 ? (arr2[y - yStep1]) : []; 140                     if(arr_find1) 141                     { 142                         arr_res0=arr_res0.concat(arr_find1); 143                     } 144                     if(arr_find2) 145                     { 146                         arr_res0=arr_res0.concat(arr_find2); 147                     } 148                     if(arr_find3) 149                     { 150                         arr_res0=arr_res0.concat(arr_find3); 151                     } 152                     if(arr_find4) 153                     { 154                         arr_res0=arr_res0.concat(arr_find4); 155                     } 156                     var len2=arr_res0.length; 157                     var nearestUnit=null; 158                     for(var j=0;j<len2;j++) 159                     { 160  161                         var obj=arr_res0[j]; 162                         if(obj.doing!="dead"&&obj.owner.name==owner_target) 163                         { 164  165                             if(!nearestUnit) 166                             { 167                                 nearestUnit=obj; 168                             } 169                             else 170                             { 171                                 if(unit2distance(obj,unit)<unit2distance(nearestUnit,unit)){ 172                                     nearestUnit=obj; 173                                 } 174                             } 175                         } 176                     } 177                     if(nearestUnit) 178                     { 179                         arr_res.push(nearestUnit); 180                     } 181  182                 } 183             } 184             if(arr_res.length>0) 185             { 186                 //arr_res=[arr_res[newland.RandomChooseFromArray(arr_res)]]; 187                 break; 188             } 189         } 190     } 191     return arr_res; 192 }//分隔线 193 function unit2distance(unit1,unit2)//两点间距离 194 { 195     return Math.pow(Math.pow((unit1.left-unit2.left),2)+Math.pow((unit1.top-unit2.top),2),0.5) 196 } 197 function unit2isat(unit,pos) 198 { 199     if(unit.left==pos.left&&unit.top==pos.top)//如果单位正好处于这个位置 200     { 201         return true; 202     } 203     else 204     { 205         return false; 206     } 207 } 208 function unit2substract(posFrom,posTo)//取两个二元向量的差向量 209 { 210     var posRes={left:posTo.left-posFrom.left,top:posTo.top-posFrom.top}; 211     return posRes; 212 } 213 function unit2normal(unit)//标准化二元向量 214 { 215     var length=Math.pow(unit.left*unit.left+unit.top*unit.top,0.5); 216     var posRes={left:unit.left/length,top:unit.top/length}; 217     return posRes; 218 } 219 function unit2times(unit,times)//二元向量伸缩 220 { 221     var posRes={left:unit.left*times,top:unit.top*times}; 222     return posRes; 223 } 224 function unit2add(unit1,unit2) 225 { 226     var posRes={left:unit1.left+unit2.left,top:unit1.top+unit2.top}; 227     return posRes; 228 } 229 function isTooNear(unit,arr,dis)//unit与arr中的对象是否过于接近,只要有一个就返回true 230 { 231     var len=arr.length; 232     if(!dis)//如果没有规定统一的最近距离, 233     { 234         for(var i=0;i<len;i++) 235         { 236             var obj=arr[i] 237             if(unit2distance(unit,obj)<(unit.clipSize+obj.clipSize)/2) 238             { 239                 return true; 240             } 241         } 242     } 243     else 244     { 245         for(var i=0;i<len;i++) 246         { 247             var obj=arr[i] 248             if(unit2distance(unit,obj)<dis) 249             { 250                 return true; 251             } 252         } 253     } 254  255 }//分隔线 256 function updatePart(unit,arr_part,arr_partowner){//更新两个索引数组 257     var x=unit.x; 258     var y=unit.y; 259     var arr_old=arr_part[x][y]; 260     var len=arr_old.length; 261     for(var i=0;i<len;i++) 262     { 263         if(arr_old[i].id==unit.id) 264         { 265             arr_old.splice(i,1); 266             arr_partowner[x][y][unit.owner.name]--; 267             break; 268         } 269     } 270     if(!arr_part[x]) 271     { 272         arr_part[x]=[]; 273     } 274     if(!arr_part[x][y]) 275     { 276         arr_part[x][y]=[]; 277     } 278     arr_part[x][y].push(unit); 279     if(!arr_partowner[x]) 280     { 281         arr_partowner[x]=[]; 282     } 283     if(!arr_partowner[x][y]) 284     { 285         arr_partowner[x][y]={}; 286     } 287     var obj_temp=arr_partowner[x][y]; 288     if(!obj_temp[unit.owner.name]) 289     { 290         obj_temp[unit.owner.name]=1; 291     } 292     else 293     { 294         obj_temp[unit.owner.name]++; 295     } 296 }

六、总结

以上完成了一个最基础的多单位思考与交互框架,在此基础上可以编辑更多种类的单位和更复杂思考方式,但框架尚存在问题,比如缺少单位间碰撞检测(或多层堆叠限制),这会导致所有单位最终重叠为一点,比如视角控制方式不流畅,比如三个循环未分离等等。