3D网页小实验-基于多线程和精灵动画实现RTS式单位行为
阅读原文时间:2021年08月11日阅读:1

一、实验目的:

1、在上一篇的“RTS式单位控制”的基础上添加逻辑线程,为每个单位实现ai计算;

2、用精灵动画为单位的行为显示对应的动作效果。

二、运行效果:

1、场景中的单位分为红蓝两方,单位在发现敌对单位后向敌人移动:

2、进入攻击范围后对敌对单位发起攻击:

注意,单位在“移动”、“攻击”、“受伤”、“死亡”时分别播放不同的动画。

3、切换为RTS式控制后,可以选择单位并发布“移动攻击”命令:

有一些单位已经与敌人接触,优先执行攻击动作。

三、程序结构:

1、工程目录:

外层ASSETS目录保存了场景的地面贴图、天空盒、地形资源,内层ASSETS目录下是单位的精灵动画资源。(实际的github仓库里还有上篇文章的代码)

2、线程结构:

主线程中的TESTRTS.html是程序入口,负责初始化WebGL场景和与逻辑线程通信,babylon50.min.js是Babylon.js引擎库,newland.js是一个Web3D工具库,One.js是单位渲染代码,VTools.js是向量计算代码,ControlRTS3.js是rts控制代码,FrameGround2.js是地形生成代码,recast.js是群组导航库。主线程负责dom管理、WebGL场景的渲染、单位的群组寻路、单位的动画计算。

逻辑线程中的worker.js负责初始化逻辑环境、维持逻辑循环和与主线程通信,OneThink.js负责单位ai计算。逻辑线程和主线程间使用postMessage进行通信。

四、主线程初始化:

在之前的文章中介绍过的内容不再赘述,这里只讨论新增的部分,可在https://github.com/ljzc002/ControlRTS下载代码,国内访问github的一种方法见附录一。

1、生成单位的精灵动画图片:

场景中使用的精灵动画图片如下:

这是一张透明背景的PNG图片,图中每个128*128的像素小块对应精灵动画的一帧,这种图片一般由美工使用专业工具绘制,但这里为演示方便用代码生成:

1
2 3 4 5 建立用于精灵动画的方块图列 6 7 8

9 10
11 12 251

这段代码首先定义出根据参数绘制人和矛的方法,然后从194行到220行画出了每一个动画帧的姿态(注意每绘制一帧后会自动移动绘制位置),linearInterpolation方法的作用是根据两个关键帧的参数插值生成中间帧的参数,比如194和204两行(对应图片的第一格和第四格)的参数是人工设计出来的,而197和200行(第二格和第三格)的参数是使用linearInterpolation方法插值得到。将linearInterpolation方法放在另一个html里可能更加条理分明,但这里为了省事就放在一个html里了。

2、主线程初始化流程:

下面介绍其中的新增部分

3、为了区分不同势力,在beforeInit方法中对原始精灵动画图进行改造:

1 function beforeInit()
2 {
3 var can_source=document.createElement("canvas");
4 var img=new Image();
5 img.src="ASSETS/002.png";
6 img.onload=function(){
7 width=img.width;
8 height=img.height;
9 can_source.style.width=width+"px";
10 can_source.style.height=height+"px";
11 can_source.width=width;
12 can_source.height=height;
13 var con_source=can_source.getContext("2d");
14 con_source.drawImage(img,0,0);
15
16 for(var i=0;i<9;i++)//用不同颜色区分不同势力
17 {
18 con_source.beginPath()
19 con_source.fillStyle="red";
20 con_source.arc(64+i*128,8,8,0,Math.PI*2,true);
21 con_source.closePath();
22 con_source.fill();
23 }
24 obj_png.a=can_source.toDataURL();
25 for(var i=0;i<9;i++)
26 {
27 con_source.beginPath()
28 con_source.fillStyle="blue";
29 con_source.arc(64+i*128,8,8,0,Math.PI*2,true);
30 con_source.closePath();
31 con_source.fill();
32 }
33 obj_png.b=can_source.toDataURL();
34 Init();
35 }
36
37
38 }

这里因为时间有限只用简单的红点和蓝点表示不同势力,更好的方案是修改单位本身的装备颜色,并且对不同势力应用不同的“选中框”。

4、webGLStart2方法除了上一篇文章中提到过的建立导航网格和导航群组外,还负责生成两种精灵管理器和“选中框”的源网格:

新增代码:

1 //initThem,两个精灵管理器分别负责绘制代表两个势力单位的精灵
2 var spriteManagerPlayerA = new BABYLON.SpriteManager("playerManagerA", obj_png.a, 200, 128, scene);
3 spriteManagerPlayerA.isPickable = true;
4 spriteManagerPlayerA.renderingGroupId=2;
5 MyGame.spriteManagerPlayerA=spriteManagerPlayerA;
6 var spriteManagerPlayerB = new BABYLON.SpriteManager("playerManagerB", obj_png.b, 200, 128, scene);
7 spriteManagerPlayerB.isPickable = true;
8 spriteManagerPlayerB.renderingGroupId=2;
9 MyGame.spriteManagerPlayerB=spriteManagerPlayerB;
10 obj_owners={a:spriteManagerPlayerA,b:spriteManagerPlayerB}
11
12 // var spriteManagerPlayerK = new BABYLON.SpriteManager("spriteManagerPlayerK", "ASSETS/kuang3.png", 400, 64, scene);
13 // spriteManagerPlayerK.isPickable = false;
14 // spriteManagerPlayerK.renderingGroupId=3;
15 // MyGame.spriteManagerPlayerK=spriteManagerPlayerK;
16 var mat_frame = new BABYLON.StandardMaterial("mat_frame", scene);
17 mat_frame.wireframe = true;
18 var mesh_k1 = BABYLON.MeshBuilder.CreatePlane("mesh_k1", {height:1,width:1}, scene);
19 //var mesh_k1=new BABYLON.MeshBuilder.CreateBox("mesh_k1",{},scene);
20 mesh_k1.renderingGroupId=2;
21 mesh_k1.isVisible=false;
22 mesh_k1.material=mat_frame;
23 mesh_k1.billboardMode=BABYLON.Mesh.BILLBOARDMODE_ALL;
24 MyGame.mesh_k1=mesh_k1;//建立一个边框材质的“选择框”

选择框的作用一是为每个单位提供一个可以被点选的网格实例(Babylon.js的精灵对象不接受射线检测),二是在选中单位后在单位周围画框,表示单位被选中(也就是第三张动图中的白框),为了提升渲染效率这里使用网格实例。

5、随机生成单位

One.js的前半部分代码:

1 var One=function(){//单位的属性
2 this.pos={x:0,y:1,z:0};
3 this.radius=0.2;
4 this.view=5;//视野(地块数)
5 this.id=null;
6 this.arr_history=[];
7 //----20210723RTS
8 this.hp=4;
9 this.at=2;
10 this.owner=null;
11 this.influence=5;
12 }
13 var bing0;
14 //在平面社交网络中建立多个One,注意他们不能重叠
15 //参数:保存One的数组,建立个数
16 One.createRandomThem=function(obj_them,count){
17 // bing0=new BABYLON.Sprite("bing0", obj_owners.a);
18 // bing0.position.y=10;
19 // bing0.playAnimation(0,8,true,100);
20
21 for(var i=0;i<count;i++)
22 {
23 var one=new One();
24 one.id="One_"+i;
25 var owner=newland.RandomChooseFromObj(obj_owners)//随机分配势力
26 one.owner=owner.key;
27 //先关联群组,然后使用群组的位置设定方法
28 var randomPos=new BABYLON.Vector3(0,0,0);
29 if(one.owner=="a")
30 {//使用导航组件的随机位置方法,可以避免单位重叠
31 randomPos = navigationPlugin.getRandomPointAround(new BABYLON.Vector3(20.0, 0.2, 0), 0.5);
32 }
33 else
34 {
35 randomPos = navigationPlugin.getRandomPointAround(new BABYLON.Vector3(-20.0, 0.2, 0), 0.5);
36 }
37     //建立一个变换节点,在Babylon.js中表示单位的位置和姿态
38 var transform = new BABYLON.TransformNode();
39 //agentCube.parent = transform;把单位添加到导航群组,用来进行群组导航
40 var agentIndex = MyGame.crowd.addAgent(randomPos, MyGame.agentParams, transform);
41     //建立一个精灵,用来显示单位
42 var bing=new BABYLON.Sprite("bing_"+one.id, owner.value);
43 //这里bing的size是默认的1!!
44 //transform.pathPoints=[transform.position];
45 var state={//单位的状态机
46 feeling:"free",
47 wanting:"waiting",
48 doing:"waiting",
49 being:{},//一个单位可能同时受到多种影响
50 }
51 transform.position=randomPos;
52 bing.position=transform.position;//精灵没有parent属性!!
53 one.pos={x:transform.position.x,y:transform.position.y,z:transform.position.z}
54 one.idx=agentIndex;
55 one.trf=transform;
56 one.mesh=bing;
57 var kuang=MyGame.mesh_k1.createInstance("k1_"+one.id);//单位的选择框
58 kuang.isVisible=false;
59 kuang.parent=transform;
60 //var kuang =new BABYLON.Sprite("sprite_kuang_"+one.id, MyGame.spriteManagerPlayerK);//显示在bing周围的白框,用来表示选中
61 //kuang.isVisible=false;
62 //kuang.size=0.8;
63 //kuang.position.y=0.5;
64 one.kuang=kuang;
65 kuang.unit=one;
66 one.target=null;
67 one.data={state:state};
68 bing.unit=one;
69 arr_unit.push(one);//分别用数组元素和对象属性的方式保存单位对象
70
71 //one.arr_history.push(obj);//记录单位的最初状态
72 obj_them[one.id]=one;
73 }
74 return obj_them;//既改变又返回
75
76 }

另一种避免单位重叠的方法:

1 One.setPos=function(obj_them,one,arr_x,arr_z)
2 {
3 one.pos.x=newland.RandomBetween(arr_x[0],arr_x[1]);
4 one.pos.z=newland.RandomBetween(arr_z[0],arr_z[1]);
5 for(var key in obj_them)
6 {
7 var olderOne=obj_them[key];
8 if(vxz.distance(olderOne.pos,one.pos)<(olderOne.radius+one.radius))//如果太近
9 {
10 One.setPos(obj_them,one,arr_x,arr_z);//第一次未失败的递归会设定pos
11 break;
12 }
13 }
14 return one;
15 }

6、启动逻辑线程

1 function initWorker()
2 {
3 //initWorker
4 console.log("开始启动work线程");
5 worker = new Worker("WORK/worker.js");
6
7 //与逻辑线程通信的代码稍后介绍
8
9 }

第五行代码直接执行worker.js,这种用法与nodejs相似。

7、启动渲染循环的代码在ControlRTS3.js文件中,ControlRTS3.js与上篇文章的区别有两处:

a、点击右键时不直接触发移动,而是将移动命令发给逻辑线程,然后由逻辑线程决定是否移动:

1 function onContextMenu(evt)
2 {
3 var pickInfo = scene.pick(scene.pointerX, scene.pointerY, (mesh)=>(mesh.id!="mesh_kuang0"), false, MyGame.camera0);
4 if(pickInfo.hit)
5 {
6 var mesh = pickInfo.pickedMesh;
7 //if(mesh.myname=="navmeshdebug")//这是限制只能点击导航网格
8 var startingPoint=pickInfo.pickedPoint;
9 //var agents = MyGame.crowd.getAgents();
10 var len=arr_selected.length;
11 var i;
12 var obj_selected={};
13 for (i=0;i<len;i++) {//分别指挥被框选中的每个单位
14 var unit=arr_selected[i];
15 //var agent=agents[unit.idx];
16 //移动可以分为攻击移动和强制移动两种,默认是攻击移动?
17 unit.data.state.doing="waiting";//正在移动《-确保开始移动后才能改为walking,也就是说只能由move方法设置!
18 unit.data.state.wanting="Attackto";//想要攻击移动
19 unit.data.state.feeling="commanded";//收到命令
20 unit.target={x:startingPoint.x,y:startingPoint.y,z:startingPoint.z}
21 //命令发出后,交给逻辑线程,逻辑线程进行判断后做出行动
22 //crowd.agentGoto(agent, navigationPlugin.getClosestPoint(startingPoint));
23 obj_selected[unit.id]=unit;
24 }
25 var obj_units0=One.obj2data(obj_selected);
26 worker.postMessage(JSON.stringify({type:"unitCommand",obj_units0:obj_units0}));
27 }
28
29 }

b、在每一帧渲染前设置精灵的朝向,并且把当前单位状态同步给逻辑线程

1 scene.registerBeforeRender(
2 function(){
3 //Think();//如果没有逻辑线程,则在这里进行ai计算
4 var obj_count={a:0,b:0};
5 if(flag_runningstate=="初始化完成")
6 {
7 var jiao_camera0=camera0.rotation.y%(Math.PI*2);//相机的姿态角
8 if(jiao_camera0<0) 9 { 10 jiao_camera0+=Math.PI*2; 11 } 12 var len = arr_unit.length; 13 //var flag_rest=false;//每个运动群组都要有专属的运动结束标志!!!! 14 var obj_send={}; 15 for(let i = 0;i0)
36 // {
37 //
38 // }
39 if(jiao_camera0-jiao_face>0&&jiao_camera0-jiao_face0)
58 {
59 obj_count[unit.owner]+=1;
60 }
61
62 }
63 var obj_units0=One.obj2data(obj_send);//将单位信息整理为json格式,发送给逻辑线程
64 worker.postMessage(JSON.stringify({type:"updateUnits",obj_units0:obj_units0}));
65 div_middle.innerHTML="红:"+obj_count.a+",蓝:"+obj_count.b;//修改红蓝势力人数
66 // if(bing0)
67 // {
68 // bing0.position.x+=0.01;
69 // bing0.invertU=!bing0.invertU
70 // }
71
72 }
73
74 }
75 )

需要注意的是,因为群组导航在主线程中进行,所以只有主线程能保有单位的准确实时状态,并每帧向逻辑线程同步,而逻辑线程则根据单位状态,向主线程发送命令,触发主线程中的动画或寻路计算。

五、逻辑线程

1、在逻辑线程中引入更多js文件

js的work线程不能操作window对象,所以引入js文件的方式与主线程有所区别,经过试验确定可以用以下方法引入其他js文件:

1 //加载其他js文件
2 var newland={}//下面代码来自百度
3 if(!newland.importScripts){ newland.importScripts=(function(globalEval){ var xhr=new XMLHttpRequest; return function importScripts(){ var args=Array.prototype.slice.call(arguments) ,len=args.length ,i=0 ,meta ,data ,content ; for(;i<len;i++){ if(args[i].substr(0,5).toLowerCase()==="data:"){ data=args[i]; content=data.indexOf(","); meta=data.substr(5,content).toLowerCase(); data=decodeURIComponent(data.substr(content+1)); if(/;\s*base64\s*[;,]/.test(meta)){ data=atob(data); } if(/;\s*charset=[uU][tT][fF]-?8\s*[;,]/.test(meta)){ data=decodeURIComponent(escape(data)); } }else{ xhr.open("GET",args[i],false); xhr.send(null); data=xhr.responseText; } globalEval(data); } }; }(eval)); }
4 newland.importScripts("OneThink.js");
5 newland.importScripts("../VTools.js");

大致思路是用ajax获取远程js文件的文本,然后用eval方法执行。注意,用这种方法加载的js文件不会默认出现在Chrome浏览器的调试页面中,在执行到对应方法时选择“步入调试”方可进入js文件内部。

2、线程间通信:

Chrome不允许多线程共享内存,所以选择postMessage方法在线程间传递JSON信息,两个线程的信息发送方式如下:

主线程:

1 worker.postMessage(JSON.stringify({type:"initWork",obj_units0:obj_units0}));

逻辑线程:

1 self.postMessage(JSON.stringify({type:"consoleLog",text:"work线程加载其他js文件完成"}));

信息中以type属性表示命令类型,其他属性保存命令的参数。

两个线程的信息接收方式如下:

主线程:

1 worker.onmessage=function(e)
2 {
3 var obj_data=JSON.parse(e.data)
4 if(obj_data.type=="consoleLog")//根据type不同使用不同处理方式,输入日志信息
5 {
6 console.log(obj_data.text);
7 }
8 else if(obj_data.type=="consoleError")//输出异常信息
9 {
10 console.error(obj_data.text);
11 }
12 else if(obj_data.type=="workInited")//逻辑线程初始化完成
13 {//
14 console.log("逻辑初始化完成");
15 count_init--;
16
17 if(count_init==0)
18 {
19 flag_runningstate="初始化完成";
20
21 }
22 }
23 else if(obj_data.type=="updateUnits")//hp等量的变化与前端的动画帧计时关系很大,所以应该用前端渲染更新后端逻辑?!
24 {//由逻辑线程向主线程同步单位状态(未使用)
25 var obj_units0=obj_data.obj_units0;
26 for(var key in obj_units0)
27 {
28 var obj0=obj_units0[key];//从Think线程传递过来的新状态
29 var obj=obj_units[key];
30 //obj.pos=obj0.pos;//位置变化由渲染线程计算
31 obj.radius=obj0.radius;
32 obj.view=obj0.view;
33 obj.hp=obj0.hp;
34 obj.at=obj0.at;
35 obj.owner=obj0.owner;
36 obj.influence=obj0.influence;
37 obj.arr_history.push(obj0);
38 }
39 console.log("完成一次计算:"+(new Date().getTime()));//obj_data.frameTime
40 }
41 else if(obj_data.type=="unitCommand")//收到逻辑线程传来命令,一般是触发动画效果和导航效果
42 {
43 var obj_units0=obj_data.obj_units0;
44 for(var key in obj_units0)//对于每一个带有命令的单位
45 {
46 var obj0=obj_units0[key];//从Think线程传递过来的新状态
47 var obj=obj_units[key];//主线程中的单位对象
48 var arr_c=obj0.arr_command;//对于这个单位携带的每一条命令
49 var len=arr_c.length;
50 for(var i=0;i<len;i++)
51 {
52 var command=arr_c[i];
53 //var func=eval(command.func)
54 obj[command.func] (command.obj_p);//方法名(参数)
55 }
56 }
57 //console.log("count_a:"+obj_data.count_a);
58 }
59 }

个人比较喜欢用if else代替switch,因为可以在判断中使用更复杂的条件。这里的通信处理代码和WebSocket通信处理代码很像,稍加修改即可将多线程计算变为网络计算,更进一步的可以建立多线程和网络相结合的“云计算”。

逻辑线程:

1 self.onmessage=function(e)
2 {
3 var obj_data=JSON.parse(e.data)
4 if(obj_data.type=="initWork")//收到主线程的“初始化逻辑线程”命令
5 {
6 self.postMessage(JSON.stringify({type:"consoleLog",text:"开始初始化work线程"}));
7 var mapWidth=maxx-minx;//地图的宽度
8 var mapHeight=maxz-minz//高度
9 var partCountX=Math.ceil(mapWidth/partSizeX);//根据预先设定的地块大小,将地图划分为多个地块
10 var partCountZ=Math.ceil(mapHeight/partSizeZ);
11 for(var i=0;i{
23 arr.forEach((obj_part,j)=>{
24 if(arr_part[i-1]&&arr_part[i-1][j])
25 {
26 obj_part[3]=arr_part[i-1][j];//west
27 }
28 else
29 {
30 obj_part[3]=null;
31 }
32 if(arr_part[i+1]&&arr_part[i+1][j])//east
33 {
34 obj_part[0]=arr_part[i+1][j]
35 }
36 else
37 {
38 obj_part[0]=null;
39 }
40 if(arr_part[i][j-1])
41 {
42 obj_part[2]=arr_part[i][j-1];//north
43 }
44 else
45 {
46 obj_part[2]=null;
47 }
48 if(arr_part[i][j+1])
49 {
50 obj_part[1]=arr_part[i][j+1];//south
51 }
52 else
53 {
54 obj_part[1]=null;
55 }
56 })
57 })
58
59 var obj_units0=obj_data.obj_units0;//从主线程传来的单位数据
60 for(var key in obj_units0)//在work线程中为每个单位建立思考对象
61 {
62 var oneThink=new OneThink(obj_units0[key])
63 obj_units[key]=oneThink;
64 }
65
66 self.postMessage(JSON.stringify({type:"consoleLog",text:"work线程单位思考对象初始化完成"}));
67
68 //建立单位区域索引和每个单位的初始影响范围
69 for(var key in obj_units)
70 {
71 var one=obj_units[key];
72 one.partx=Math.floor((one.pos.x-minx)/partSizeX);
73 one.partz=Math.floor((one.pos.z-minz)/partSizeZ);
74 arr_part[one.partx][one.partz].arr_unit.push(one);//把单位对象放到地块对象的arr_unit属性中
75
76 }
77 self.postMessage(JSON.stringify({type:"consoleLog",text:"work线程单位区域索引初始化完毕"}));
78
79
80 //self.postMessage(JSON.stringify({type:"updateUnits",obj_units0:obj_units0}));
81 self.postMessage(JSON.stringify({type:"workInited"}));
82 Loop();//这里不等主线程命令直接启动逻辑循环
83
84 }
85 else if(obj_data.type=="command")//主线程发来命令,可以直接执行逻辑线程中的全局方法
86 {
87 var func=eval(obj_data.func);//对于方法对象
88 var obj_p=obj_data.obj_p;
89 //eval(func+"("+obj_p+")");//直接这样执行obj_p会被强制转换为字符串类型!!!!
90 func(obj_p)
91 }
92 else if(obj_data.type=="setValue")//直接设置逻辑线程中的全局变量
93 {
94 //var key=eval(obj_data.key);//对于直接量则会变成值而非指针!!!!
95 var key=obj_data.key
96 var value=obj_data.value;
97 eval(key+"="+value);
98 //key=value;
99 }
100 else if(obj_data.type=="updateUnits")//主线程同步单位状态
101 {
102 var obj_units0=obj_data.obj_units0;
103 for(var key in obj_units0)
104 {
105 var obj0=obj_units0[key];//从渲染线程传递过来的新状态
106 var obj=obj_units[key];
107 obj.doing=obj0.doing;//正在做的事
108 //obj.wanting=obj0.wanting;//想要做的事
109 //this.being={};//正在遭受
110 //obj.feeling=obj0.feeling;//命令通过unitCommand传递!
111 obj.hp=obj0.hp;
112 obj.pos=obj0.pos;//位置变化由渲染线程中的群组导航计算
113 var partx=Math.floor((obj.pos.x-minx)/partSizeX);
114 var partz=Math.floor((obj.pos.z-minz)/partSizeZ);
115 if(partx!=obj.partx||partz!=obj.partz)//如果位置索引发生变化,则把单位放到新的地块中
116 {
117 var arr_unit=arr_part[obj.partx][obj.partz];
118 var len=arr_unit.length;
119 for(var i=0;i<len;i++)
120 {
121 if(arr_unit[i].id==obj.id)
122 {
123 arr_unit.splice(i,1);
124 break;
125 }
126 }
127 obj.partx=partx;
128 obj.partz=partz;
129 arr_part[partx][partz].arr_unit.push(obj);
130 }
131
132 }
133 }
134 else if(obj_data.type=="unitCommand")//主线程发来单位命令
135 {
136 var obj_units0=obj_data.obj_units0;
137 for(var key in obj_units0)
138 {
139 var obj0 = obj_units0[key];//从渲染线程传递过来的新状态
140 var obj = obj_units[key];
141 obj.target=obj0.target;//目标地点
142 obj.doing=obj0.doing;//正在做的事
143 obj.wanting=obj0.wanting;//想要做的事
144 //this.being={};//正在遭受
145 obj.feeling=obj0.feeling;
146 }
147 }
148 }

逻辑线程的信息接收代码中也包含了逻辑线程的初始化代码。逻辑线程的初始化除了根据主线程发来的单位信息在逻辑线程中建立单位对象外,还将逻辑线程中的地图划分为棋盘状排列的地块,并把每个单位和其所在的地块关联,这样我们就能较为快捷的根据地块关系找到每个单位“附近的”单位,而不用对地图上的所有单位进行遍历。注意,当单位移动到其他地块时,要修改单位和地块的对应关系。

另外,因为主线程的单位对象中包含不需要传递给逻辑线程的Babylon.js渲染数据(比如精灵动画、群组、变换节点),在向逻辑线程同步单位状态前要进行一次简化处理:

1 //把单位对象集群转化为易于传输的格式,主要是剔除了网格信息
2 One.obj2data=function(obj_them){
3 var obj_send={};
4 for(var key in obj_them)
5 {
6 var one=obj_them[key];
7 var obj_p={
8 pos:one.pos,
9 radius:one.radius,
10 view:one.view,
11 id:one.id,
12 hp:one.hp,
13 at:one.at,
14 owner:one.owner,
15 influence:one.influence,
16 doing:one.data.state.doing,
17 wanting:one.data.state.wanting,
18 feeling:one.data.state.feeling,
19 target:one.target,
20 }
21 obj_send[key]=obj_p;
22 }
23 return obj_send;
24 }

3、逻辑循环:

在worker.js中启动逻辑循环:

1 var count_a=0;
2 function runOneStep(){//一次逻辑运算
3 count_a=0;
4 if(flag_thinking)//正在进行上一次思考
5 {
6 self.postMessage(JSON.stringify({type:"consoleError",text:"正在进行上一次思考"}));
7 return;
8 }
9 flag_thinking=true;
10 var startTime=new Date().getTime();
11 //self.postMessage(JSON.stringify({type:"consoleLog",text:"开始计算影响力:"+startTime}));
12 OneThink.clearAllInfluence(arr_part);//清除每个单位对地块的影响力
13 for(var key in obj_units)//影响
14 {
15 var unit=obj_units[key];
16 if(unit.doing!="dead"&&unit.doing!="unconscious") {
17 OneThink.oneMakeInfluence(unit, arr_part, partSize);//重新计算每个单位对地块的影响力,这件事每一周期都要做
18 }
19 }
20 //self.postMessage(JSON.stringify({type:"consoleLog",text:"开始思考:"+(new Date().getTime())}));
21 var obj_units0={};
22 for(var key in obj_units)//思考
23 {
24 var unit=obj_units[key];
25 if(unit.hp<=0) 26 { 27 unit.doing="dead"; 28 } 29 if(unit.doing!="dead"&&unit.doing!="unconscious"&&unit.doing!="attacking") 30 { 31 var arr_command=OneThink.think(unit,obj_units,arr_part,partSize);//单位进行思考,得出要发送给主线程的命令 32 if(arr_command.length>0)
33 {
34 obj_units0[unit.id]={arr_command:arr_command};//.arr_command=arr_command;
35 }
36
37 }
38 }
39 self.postMessage(JSON.stringify({type:"unitCommand",obj_units0:obj_units0,count_a:count_a}));//把命令发送给主线程
40
41 var endTime=new Date().getTime();
42 //self.postMessage(JSON.stringify({type:"consoleLog",text:"完成思考:"+(endTime-startTime)}));
43 flag_thinking=false;
44 }
45 //Loop指令由前端发来
46 var flag_autorun=true;
47 var lastframe=new Date().getTime();
48 function Loop()//逻辑循环
49 {
50 if(flag_autorun)
51 {
52 runOneStep();
53 var thisframe=new Date().getTime();
54 //self.postMessage(JSON.stringify({type:"consoleLog",text:thisframe-lastframe}));//把历史演变保存在哪里?
55 //console.log(thisframe-lastframe,"red:"+obj_owners.red.countAlive,"blue:"+obj_owners.blue.countAlive);
56 lastframe=thisframe;
57 }
58 //self.requestAnimationFrame(function(){Loop()});
59 self.setTimeout(function(){Loop()},500)//限制逻辑密度
60 }

这里首先计算每个单位的影响范围,接着计算单位在受到影响后的行为。

4、计算单位影响范围

这里“单位侦察到其他单位”的算法是:被侦察单位根据其影响力的不同,对不同范围的地块造成影响,如果受到影响的地块在侦察单位的视野范围内,则认为侦察者可以发现该单位。

因此在每次逻辑计算中都需要计算单位的影响范围(代码在OneThink.js文件中):

1 //在计算每个单位的当前影响范围前,先清空旧的影响范围《-影响范围变化率如果很低则这个计算会比较冗余
2 OneThink.clearAllInfluence=function(arr_part){//影响范围也是保存在地块对象中的
3 arr_part.forEach((arr,i)=>{
4 arr.forEach((obj_part,j)=>{
5 obj_part.arr_influence=[];
6 })
7 })
8 }
9 OneThink.oneMakeInfluence=function(unit,arr_part,partSize){
10 //考虑到不同的单位被发现的可能性不同,所以不能只从观察者向周围看,要先用被观察者对周围造成影响
11 //对于自己影响到的地块注入影响力
12 var arr_part_found=OneThink.getArrPartbyStep(arr_part[unit.partx][unit.partz],unit.influence);
13 arr_part_found.forEach((obj)=>{//单位对周围的地块造成影响
14 for(var xz in obj)
15 {
16 var obj_part=obj[xz];
17 obj_part.arr_influence.push(unit);
18 }
19 })
20 }

OneThink.getArrPartbyStep方法的作用是,从一个地块对象出发,寻找指定距离内的所有地块:

1 //寻找地块应该是直接遍历空间内的所有地块索引,还是根据规则从起点地块一圈一圈查找?
2 //这取决于一个线程能够负担多少单位的计算,假设一个地块平均保有100个单位,估计一个线程最多负责计算10000个单位,也就是最多可能有100个地块,10*10排列,最大步数不超过20
3 //根据步数寻找单位一定范围内的地块
4 OneThink.getArrPartbyStep=function(obj_part,step_influence)
5 {
6 var arr_part_found=[];
7 //var depth=0;
8 for(var i=0;i900)
31 // {
32 // depth+=2
33 // }
34 // }
35 // }
36 // }
37 OneThink.arr_direction.forEach((to,index)=>{//对于地块的东南北西每个方向
38 if((to+from)!=3||from=="o")//比如从左向右进入下一个地块,则下一个地块不应重复检测自身的西方,而从起点出发时要检测所有方向
39 {
40 var obj_part2=obj_part[to];//之前初始化逻辑线程时保存了每个地块的周边地块信息
41 if(obj_part2)出发地块旁边的一个地块
42 {
43 if(!arr_part_found[depth][obj_part2.partx+"_"+obj_part2.partz])//每个地块只添加一次
44 {//用地块索引作为属性名!
45 arr_part_found[depth][obj_part2.partx+"_"+obj_part2.partz]=(obj_part2);
46 //depth+=1;//再取下一层地块
47 OneThink.getArrPartbyStep2(obj_part2,step_influence,to,depth+1,arr_part_found);
48 }
49
50 }
51 }
52 })
53
54
55 }
56
57 }

如果使用东南北西的计算方法,通过数字索引的加减也可以遍历周围的地块,但代码会更复杂一些。

5、单位的思考

1 //参数:单位对象、其他所有单位对象、地块索引表
2 OneThink.think=function(unit,obj_units,arr_part){
3 var arr_command=[];//返回的命令数组
4 if(unit.feeling=="free")//单位自由行动时
5 {
6 var obj_part=arr_part[unit.partx][unit.partz];
7 //遍历所有和自己处于同一地块的单位,以及虽处于其他地块但影响到这一地块的单位,这里假定view为1?!
8 //var arr_neighbor=obj_part.arr_unit;//单位肯定会影响自己所在的地块,所以这个其实没有用
9 var dis_min=9999;//最小距离
10 var unit_nearest=null;//最近单位
11
12 // arr_neighbor.forEach((neighbor,i)=>{
13 // if(neighbor.id!=unit.id&&neighbor.owner!=unit.owner)
14 // {
15 // var dis=vxz.distance(unit.pos,neighbor.pos);
16 // if(dis_min>dis)
17 // {
18 // dis_min=dis;
19 // unit_nearest=neighbor;
20 // }
21 // }
22 // })
23 var arr_part_found=OneThink.getArrPartbyStep(obj_part,unit.view);//这个是从0层到4层的!《-可以优化
24 var len =arr_part_found.length;
25 for(var i=0;i{
34 if(neighbor.id!=unit.id&&neighbor.owner!=unit.owner)//找到的不是自己,且不是本势力
35 {
36 var dis=vxz.distance(unit.pos,neighbor.pos);//两点间距离
37 if(dis_min>dis)
38 {
39 dis_min=dis;
40 unit_nearest=neighbor;
41 }
42 }
43 })
44 }
45 if(obj2)
46 {
47 for(var xz in obj2)
48 {
49 var obj_part2=obj2[xz];
50 var arr_star=obj_part2.arr_influence;
51 arr_star.forEach((neighbor,j)=>{
52 if(neighbor.id!=unit.id&&neighbor.owner!=unit.owner)
53 {
54 var dis=vxz.distance(unit.pos,neighbor.pos);
55 if(dis_min>dis)
56 {
57 dis_min=dis;
58 unit_nearest=neighbor;
59 }
60 }
61 })
62 }
63 }
64 if(unit_nearest)//如果在内层找到最近单位,就不用去外层查看了
65 {//正确的做法应该是先查看0、1、2三层地块,如果没找到再循环+2向外遍历,这里的做法是不正确的!
66 break;
67 }
68 }
69 // arr_part_found.forEach((obj)=>{
70 // for(var xz in obj)
71 // {
72 // var obj_part2=obj[xz];
73 // var arr_star=obj_part2.arr_influence;
74 // arr_star.forEach((neighbor,i)=>{
75 // if(neighbor.id!=unit.id&&neighbor.owner!=unit.owner)
76 // {
77 // var dis=vxz.distance(unit.pos,neighbor.pos);
78 // if(dis_min>dis)
79 // {
80 // dis_min=dis;
81 // unit_nearest=neighbor;
82 // }
83 // }
84 // })
85 // }
86 // })
87
88
89 if(unit_nearest)//如果找到了最近敌对单位
90 {  //如果在攻击范围内
91 if(dis_min0.1)
102 {//两次移动目标有一定差别(比如原先要去的地方的敌人已经死了)才重新发布移动命令,否则保持原移动命令不变
103 arr_command.push({func:"move",obj_p:unit_nearest.pos})
104 }
105 }
106 else
107 {
108 arr_command.push({func:"move",obj_p:unit_nearest.pos})
109 }
110 unit.last_post=unit_nearest.pos;
111 }
112 }
113 }
114 else if(unit.feeling=="commanded")//如果目前单位正在执行命令
115 {
116 if(unit.wanting=="Attackto")//攻击移动
117 {
118 //先判断到没到目标位置!
119 var dis=vxz.distance(unit.pos,unit.target);
120 if(dis{
140 if(neighbor.id!=unit.id&&neighbor.owner!=unit.owner)
141 {
142 var dis=vxz.distance(unit.pos,neighbor.pos);
143 if(disdis)
146 {
147 dis_min=dis;
148 unit_nearest=neighbor;
149 }
150 }
151 }
152 })
153 }
154 }
155 if(unit_nearest)//如果找到了最近敌对单位
156 {
157 arr_command.push({func:"attack",obj_p:unit_nearest.id});
158 //count_a++;//注意,单位的wanting并未变化,所以在干掉周围的敌人后应该会继续向命令目的地攻击移动
159 }
160 else//移动到目标附近
161 {
162
163 if(unit.doing!="walking")//如果正在移动,则不重复命令
164 {
165 arr_command.push({func:"move",obj_p:unit.target});
166 }
167
168 }
169 }
170 else if(unit.wanting=="Forceto"){//强制移动
171 //先判断到没到目标位置!
172 var dis=vxz.distance(unit.pos,unit.target);
173 if(dis<unit.radius)//到达目标附近
174 {
175 unit.wanting="waiting";
176 unit.feeling="free";
177 return [];
178 }
179 if(unit.doing!="walking")//如果正在移动,则不重复命令
180 {
181 arr_command.push({func:"move",obj_p:unit.target});
182 }
183 }
184 }
185 return arr_command;
186
187 }

总的来讲就是根据单位的不同状态,寻找不同的目标,移动或攻击或什么也不做。

6、主线程根据单位的思考结果触发动画或导航指令(在One.js文件中):

1 One.prototype.attack=function(targetid)//攻击指令
2 {
3 var that=this;
4 var target=obj_units[targetid];
5 if(target.hp<=0)//如果攻击目标已经死亡 6 { 7 this.data.state.doing="waiting"; 8 return; 9 } 10 this.face=target.trf.position.subtract(this.trf.position); 11 // if(target.trf.position.x-this.trf.position.x<0) 12 // { 13 // this.mesh.invertU=true; 14 // } 15 // else 16 // { 17 // this.mesh.invertU=false; 18 // } 19 //this.mesh.stopAnimation();//停止之前的行走动画,否则无法进入攻击动画《-启动下个play会自动停止上一个 20 if(this.data.state.doing!="attacking")//等待上次攻击完成 21 { 22 this.data.state.doing="attacking"; 23 //同时也应该停止寻路移动!?《-是的,否则会继续向远处走 24 MyGame.crowd.agentTeleport(this.idx, this.mesh.position);//recast的群组并没有“停止导航”的功能,但可以使用“传送”方法使导航停止 25 this.mesh.playAnimation(0, 6, false, 200,function(){//动画结束后恢复思考能力 26 that.data.state.doing="waiting"; 27 var target=obj_units[targetid]; 28 if(target.data.state.doing!="dead") 29 { 30 target.hp-=2; 31 var side; 32 if(that.mesh.invertU==true) 33 { 34 side="左";//攻击方向,受到攻击的单位会倒向对应方向。 35 } 36 else 37 { 38 side="右"; 39 } 40 if(target.hp>0)
41 {//如果攻击目标的生命大于0触发受伤动画
42 target.hurt(side);
43 }
44 else
45 {//否则触发死亡处理
46 target.mesh.stopAnimation();
47 target.data.state.doing="dead";
48 target.data.state.being={};
49 target.data.state.wanting="dead";
50 target.data.state.feeling="dead";
51 target.dead(side);
52 }
53 }
54 });
55 }
56
57
58
59 }
60 One.prototype.move=function(pos)//移动命令
61 {
62 var agents = MyGame.crowd.getAgents();
63
64 var pos_t=new BABYLON.Vector3(pos.x,pos.y,pos.z);
65 //this.face=target.trf.position.subtract(this.trf.position);
66 // if(pos_t.x-this.trf.position.x<0)
67 // {
68 // this.mesh.invertU=true;
69 // }
70 // else
71 // {
72 // this.mesh.invertU=false;
73 // }
74 if(!this.mesh.animationStarted)//过于频繁的调用动画,看起来就像没有动画!!并且每个动画都不会结束,动画的回调也不会触发!
75 {//
76 this.mesh.playAnimation(6, 8, true, 100);
77 }
78
79 //MyGame.crowd.agentGoto(agents[this.idx], navigationPlugin.getClosestPoint(pos_t));
80 MyGame.crowd.agentGoto(this.idx, navigationPlugin.getClosestPoint(pos_t));//群组导航
81 }
82 One.prototype.hurt=function(side){//受伤动画
83 //scene.stopAnimation();
84 var ani=new BABYLON.Animation("animation_hurt_"+this.id,"angle",30
85 ,BABYLON.Animation.ANIMATIONTYPE_FLOAT,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
86 var keys=[{frame:0,value:0}
87 ,{frame:15,value:side=="左"?Math.PI/4:-Math.PI/4},{frame:30,value:0}];
88 ani.setKeys(keys);
89 this.mesh.animations.push(ani);//把精灵旋转一下再回来表示受到攻击,注意这种“网格的”动画和精灵动画的区别
90 scene.beginAnimation(this.mesh,0,30,false,1 );
91 }
92 One.prototype.dead=function(side){//死亡动画
93 var ani=new BABYLON.Animation("animation_hurt_"+this.id,"angle",30
94 ,BABYLON.Animation.ANIMATIONTYPE_FLOAT,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
95 var keys=[{frame:0,value:0}
96 ,{frame:30,value:side=="左"?Math.PI/2:-Math.PI/2}];
97 ani.setKeys(keys);
98 this.mesh.animations.push(ani);
99 var that=this;
100 scene.beginAnimation(this.mesh,0,30,false,1 ,function(){//死亡动画结束后释放这个单位的资源
101 // this.data.state.doing="dead";
102 // this.data.state.being={};
103 // this.data.state.wanting="dead";
104 // this.data.state.feeling="dead";
105 MyGame.crowd.removeAgent(that.idx);//移除导航物体(否则尸体会阻碍寻路),但仍保持精灵显示
106 MyGame.crowd.update();
107
108 setTimeout(function(){
109 that.trf.dispose();
110 that.mesh.dispose();
111 that.kuang.dispose();
112 },2000)
113 });
114 }

绝大部分单位属性由主线程修改,所以在主线程插入代码即可实现作弊。

六、总结

如此,实现了单位动画和ai功能,下一步可以尝试添加射弹类范围攻击效果和村民的建筑、采集效果。

附录一:

在国内访问github的一种方法:

访问DNS查询服务,比如http://www.webkaka.com/dns/

查询github.global.ssl.fastly.net的DNS解析:

查看下面的列表:

发现北京电信能够解析这个域名,于是将本机网络连接的DNS设为,北京电信的IP(203.196.0.6)

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器