网页小实验——在平面空间建立大量“可思考的”对象
实验目标:建立大量对象(万级),为每个对象设置自身逻辑,并实现对象之间的交互,以原生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 }
六、总结
以上完成了一个最基础的多单位思考与交互框架,在此基础上可以编辑更多种类的单位和更复杂思考方式,但框架尚存在问题,比如缺少单位间碰撞检测(或多层堆叠限制),这会导致所有单位最终重叠为一点,比如视角控制方式不流畅,比如三个循环未分离等等。
