查看: 1978|回复: 3
收起左侧

[已翻译] 使用群体行为创建一个冰球游戏的AI(一):基础

[复制链接]

[已翻译] 使用群体行为创建一个冰球游戏的AI(一):基础[复制链接]

初末 发表于 2017-12-25 14:49:34 [显示全部楼层] |只看大图 回帖奖励 |倒序浏览 |阅读模式 回复:  3 浏览:  1978
译者:张华栋(wcby)      审校:陈敬凤(nunu)
这篇文章是《使用群体行为创建一个冰球游戏的AI的第一部分,这个系列共4篇文章,将用详细的步骤描述如何构建一个冰球游戏的AI部分。
任何一种特定类型的游戏总有不同的方法来实现。通常开发者会选择他最擅长的技能,使用他已知的技术来尽可能的输出最好的结果。但有的时候人们并不知道他们需要一个特定的技术,甚至有可能这个技术更简单效果也更好,他们只是用他们的已知的办法来创建游戏,而这种方法可能很麻烦效果也非常糟糕。
在这个系列教程里,你将学到如何用一系列技术,比如我以前介绍过概念的群体行为,来为一个冰球游戏构建人工智能部分。

[url=]请注意: 尽管本教程的编程语言基于AS3和Flash[/url]的,但相同的技术和概念完全可以被应用到任何游戏开发环境中。当然前提是,你对本文涉及的知识和数学的向量有基本的理解。
简单的介绍
[url=]冰球是一项有趣又非常受欢迎的运动,同时,如果作为一个视频游戏来说,它融合了很多游戏开发的主题,比如运动模式、团队合作(团队攻击、防御)、人工智能以及战术战略。一个好玩的冰球游戏会非常适合用来展示这些有用的技术的融合效果。[/url]
要模拟冰球的运动机制,还要包括运动员的奔跑以及移动,这是一个很大的挑战。如果运动员的移动模式是预先设定的,即使设定了不同的移动路径,冰球游戏也会变得可预测从而枯燥无聊。我们如何实现一个充满了动态移动的游戏同时我们仍然保持着对游戏进程的控制呢?答案就是使用群体行为
群体行为的目标是创建一个充满即兴导航的真实运动模式。它们是基于在每次游戏调用update函数后调整的力作用,所以它们很自然的非常动态化。这使得群体行为非常适合用来实现冰球或者橄榄球这种复杂又充满动态变化的游戏。
[url=]确定工作的范围[/url]
因为这个教程的目的是为了教学,同时也是考虑了时间的因素,让我们把游戏涉及的范围变小一点。我们的冰球游戏将只支持这项运动原始规则的一小部分:在我们的游戏中没有裁判也没有守门员,这样每位运动员都可以在冰球比赛场四处移动。
DSC0000.jpg
使用简化后规则的冰球游戏
球门将会用一小段没有球网的墙来代替。如果想要得分,一个球队必须移动冰球(图中的黑色盘子)让它碰到对方球门的任意部分。如果某人得分了,两支队伍将会重新回到初始位置,冰球也会被放到球场的中间,比赛将在几秒后重新开始。
对于冰球的处理:如果一个运动员,不妨称他为A,现在控制着冰球,然后与另外一个运动员,不妨称他为B,发生了接触,然后B抢走了冰球,而A无法移动几秒钟。如果冰球离开了冰球比赛区域,那么它将被立刻放置在冰球比赛区域的中心。

我将使用Flixel这款游戏引擎来负责代码的图形部分。但是,在例子中的擎代码将会被化或者忽略,以便不会分散注意力从而集中思考游戏本身。
构建游戏环境
让我们从构建游戏环境开始,游戏环境包括一个冰球比赛场、几位运动员还有2个球门。球场冰区的四周是4个矩形,这些矩形会把所有与它们发生触碰的东西挡住,所以没有任何东西可以离开冰区。
一名运动员可以用Athlete类来描述:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em]22

[size=1em]23

[size=1em]24

[size=1em]25

[size=1em]26

[size=1em]27

[size=1em]28

[size=1em]29

[size=1em]30

[size=1em]31

[size=1em]32

[size=1em][size=1em]public class Athlete
[size=1em]{   
[size=1em]    private var mBoid :Boid; // controls the steering behavior stuff
[size=1em]    private var mId   :int;  // a unique identifier for the athelete
[size=1em]      
[size=1em]    public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
[size=1em]        mBoid = new Boid(thePosX, thePosY, theTotalMass);
[size=1em]    }
[size=1em]      
[size=1em]    public function update():void {
[size=1em]        // Clear all steering forces
[size=1em]        mBoid.steering = null;
[size=1em]         
[size=1em]        // Wander around
[size=1em]        wanderInTheRink();
[size=1em]  
[size=1em]        // Update all steering stuff
[size=1em]        mBoid.update();
[size=1em]    }
[size=1em]      
[size=1em]    private function wanderInTheRink() :void {
[size=1em]        var aRinkCenter :Vector3D = getRinkCenter();
[size=1em]         
[size=1em]        // If the distance from the center is greater than 80,
[size=1em]        // move back to the center, otherwise keep wandering.
[size=1em]        if (Utils.distance(this, aRinkCenter) >= 80) {
[size=1em]            mBoid.steering = mBoid.steering + mBoid.seek(aRinkCenter);
[size=1em]        } else {
[size=1em]            mBoid.steering = mBoid.steering + mBoid.wander();
[size=1em]        }
[size=1em]    }
[size=1em]}





成员变量mBoid是Boid类的一个实例,用来对群体行为系列使用的数据逻辑进行一个封装。mBoid实例与其他元素一起,用数学变量来描述实体的当前方向、群体力和位置。
在游戏每次进行update更新的时候都会调用Athlete类的update方法。现在这个方法仅仅是清理掉任何还存在的群体力,然后加一个徘徊力,最后调用下mBoid.update()。之前的命令会更新所有封装在mBoid里面的群体行为逻辑,让运动员移动(使用欧拉积分)。
负责整个游戏循环的Game类会调用PlayState。Game类有冰球比赛场、两组运动员(一方一个队伍)和两个球门:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em]22

[size=1em]23

[size=1em][size=1em]public class PlayState
[size=1em]{
[size=1em]    private var mAthletes  :FlxGroup;
[size=1em]    private var mRightGoal :Goal;
[size=1em]    private var mLeftGoal  :Goal;
[size=1em]  
[size=1em]    public function create():void {
[size=1em]        // Here everything is created and added to the screen.
[size=1em]    }
[size=1em]         
[size=1em]    override public function update():void {
[size=1em]        // Make the rink collide with athletes
[size=1em]        collide(mRink, mAthletes);
[size=1em]         
[size=1em]        // Ensure all athletes will remain inside the rink.
[size=1em]        applyRinkContraints();
[size=1em]    }
[size=1em]      
[size=1em]    private function applyRinkContraints() :void {
[size=1em]        // check if athletes are within the rink
[size=1em]        // boundaries.
[size=1em]    }
[size=1em]}




假设一个运动员被加入到比赛中,下面是到代码写到这里会得到的结果:
让运动员跟随鼠标光标移动
运动员一定要跟随鼠标光标移动,这样玩家才可以部分的控制运动员。因为鼠标光标在屏幕上有位置,它可以被用作到达行为的目的地。
到达行为会让一个运动员会去追随鼠标光标的位置,当你靠近鼠标光标位置的时候会平滑的降低速度,并最终停在那个位置上。
[url=]在[/url]Athlete 类中,让我们用到达行为来代替徘徊方法:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em][size=1em]public class Athlete
[size=1em]{   
[size=1em]    // (...)
[size=1em]      
[size=1em]    public function update():void {
[size=1em]        // Clear all steering forces
[size=1em]        mBoid.steering = null;
[size=1em]         
[size=1em]        // The athlete is controlled by the player,
[size=1em]        // so just follow the mouse cursor.
[size=1em]        followMouseCursor();
[size=1em]  
[size=1em]        // Update all steering stuff
[size=1em]        mBoid.update();
[size=1em]    }
[size=1em]      
[size=1em]    private function followMouseCursor() :void {
[size=1em]        var aMouse :Vector3D = getMouseCursorPosition();
[size=1em]        mBoid.steering = mBoid.steering + mBoid.arrive(aMouse, 50);
[size=1em]    }
[size=1em]}




最后的结果是运动员会到达鼠标光标的位置。因为运动逻辑是基于群体行为的,运动员会在冰球场上用一种让人信服和平滑的方式导航。
使用鼠标光标来引导一位运动员的demo如下:
添加和控制冰球
冰球会有Puck类来表示。Puck类最重要的部分是update()方法和mOwner属性。
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em][size=1em]public class Puck
[size=1em]{
[size=1em]    public var velocity :Vector3D;
[size=1em]    public var position :Vector3D;
[size=1em]    private var mOwner :Athlete;    // the athlete currently carrying the puck.
[size=1em]         
[size=1em]    public function setOwner(theOwner :Athlete) :void {
[size=1em]        if (mOwner != theOwner) {
[size=1em]            mOwner = theOwner;
[size=1em]            velocity = null;
[size=1em]        }
[size=1em]    }
[size=1em]         
[size=1em]    public function update():void {
[size=1em]    }
[size=1em]         
[size=1em]    public function get owner() :Athlete { return mOwner; }
[size=1em]}




跟运动的逻辑部分一样,Puck类的update函数也会在游戏每次update的时候调用一次。mOwner变量决定了冰球是否被某一个运动员控制。如果mOwner为null,这意味着冰球目前是自由的,可以移动,最后会被冰球比赛场的围墙反弹回来。
如果mOwner不为null,这意味着冰球目前被某个运动员控制着。在这种情况下,它会忽略任何的碰撞检测并且始终位于运动员的前方。这通过获取运动员的velocity向量以及运动员的方向来做到。
DSC0001.gif
让我们来解释下冰球是如何放置在运动员前方的。ahead向量是运动员velocity向量的一份拷贝,所以他们指向同一个方向。ahead向量被归一化后,可以被放缩成任何值,比如30,来控制冰球位于运动员前方多远。
最钟,冰球的position是运动员position变量加上ahead向量,这样就把冰球放到了想要的位置上。
下面是刚才讨论的全部代码:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em][size=1em]public class Puck
[size=1em]{
[size=1em]    // (...)
[size=1em]      
[size=1em]    private function placeAheadOfOwner() :void {
[size=1em]        var ahead :Vector3D = mOwner.boid.velocity.clone();
[size=1em]         
[size=1em]        ahead = normalize(ahead) * 30;
[size=1em]        position = mOwner.boid.position + ahead;
[size=1em]    }
[size=1em]      
[size=1em]    override public function update():void {
[size=1em]        if (mOwner != null) {
[size=1em]            placeAheadOfOwner();
[size=1em]        }
[size=1em]    }
[size=1em]      
[size=1em]    // (...)
[size=1em]}




在PlayState类里,会做碰撞检测来判断冰球是否与任意一个运动员重叠。如果发生了重叠,那么与冰球重叠的运动员将成为冰球的控制者。结果是冰球会粘着这个运动员。在下面的demo中,引导运动员去碰冰球比赛场中间位置的冰球看下这个效果:
让冰球被冰球杆击打
是时候让冰球杆击打冰球来让冰球移动了。无论运动员如何控制着冰球,我们所需要做的就是当冰球杆击中球的时候来计算一个新的速度向量。新的速度向量会让冰球往期望的位置移动。
[url=]速度向量可以由2个位置向量之间的差值产生,这样产生出来的向量就会从一个位置指向另外一个位置。这正好是需要计算的冰球新速度,在每次击打以后冰球的速度都要重新计算。[/url]
DSC0002.gif 冰球杆对冰球进行击打后,对冰球新速度的计算
在上面的图中,目标点就是鼠标光标。冰球现在的位置可以作为运动的起点,在被冰球杆击打后冰球应该到达的位置可以被视为终点。
下面的伪代码是关于goFromStickHit()的一个实现,goFromStickHit是Puck类的一个方法,用来实现上图中说明的逻辑。
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em][size=1em]public class Puck
[size=1em]{
[size=1em]    // (...)
[size=1em]  
[size=1em]    public function goFromStickHit(theAthlete :Athlete, theDestination :Vector3D, theSpeed :Number = 160) :void {
[size=1em]        // Place the puck ahead of the owner to prevent unexpected trajectories
[size=1em]        // (e.g. puck colliding the athlete that just hit it)
[size=1em]        placeAheadOfOwner();
[size=1em]         
[size=1em]        // Mark the puck as free (no owner)
[size=1em]        setOwner(null);
[size=1em]         
[size=1em]        // Calculate the puck's new velocity
[size=1em]        var new_velocity :Vector3D = theDestination - position;
[size=1em]        velocity = normalize(new_velocity) * theSpeed;
[size=1em]    }
[size=1em]}




new_velocity向量是从冰球的当前位置指向到目标点(theDestination)。然后,对new_velocity向量进行归一化并用theSpeed进行放缩,theSpeed定义了new_velocity向量的大小。换句话说,这个操作定义了冰球将以多快的速度从当前位置向目的地移动。最后,最后,冰球的velocity向量的值由new_velocity所取代
在PlayState类里,goFromStichHit()方法会在每次玩家点击屏幕的时候被调用。当玩家点击屏幕的时候,鼠标光标会被用作点击的目标点。结果可以看下面的demo:
添加人工智能部分
到目前为止,我们只有一个运动员在冰球赛场中移动。随着更多的运动员被添加进来,人工智能必须能够支持让所有的运动员都看上去像是活人并且有思考能力。
要达到这个目标,我们将使用基于堆栈的有限状态机 (stack-based FSM)。正如之前描述的,有限状态机是实现游戏人工智能的通用方法并且非常有用。
对于我们的冰球游戏,一个名为mBrain的属性会被加到Athlete类里面:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em][size=1em]public class Athlete
[size=1em]{   
[size=1em]    // (...)
[size=1em]    private var mBrain :StackFSM; // controls the AI stuff
[size=1em]      
[size=1em]    public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
[size=1em]        // (...)
[size=1em]        mBrain = new StackFSM();
[size=1em]    }
[size=1em]      
[size=1em]    // (...)
[size=1em]}




属性mBrain是一个StackFSM的实例,StackFSM是一个之前在FSM教程里面用过的类。它使用了一个堆栈来控制实体的人工智能状态。每一个状态用一个方法进行描述。当一个状态被压入堆栈的时候,它就成为当前激活的状态,并且在每一次游戏更新进行Update调用的时候都会被调用到。
每个状态都会执行一个特定的任务,比如将运动员往冰球位置移动。每个状态自己负责结束自己,这意味着状态自己会负责把它自己从堆栈中弹出来。
现在运动员既可以被玩家控制,也可以被人工智能控制,所以Athlete类的update函数需要被修改下来适应这个情况:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em]22

[size=1em]23

[size=1em][size=1em]public class Athlete
[size=1em]{   
[size=1em]    // (...)
[size=1em]      
[size=1em]    public function update():void {
[size=1em]        // Clear all steering forces
[size=1em]        mBoid.steering = null;
[size=1em]         
[size=1em]        if (mControlledByAI) {
[size=1em]            // The athlete is controlled by the AI. Update the brain (FSM) and
[size=1em]            // stay away from rink walls.
[size=1em]            mBrain.update();
[size=1em]              
[size=1em]        } else {
[size=1em]            // The athlete is controlled by the player, so just follow
[size=1em]            // the mouse cursor.
[size=1em]            followMouseCursor();
[size=1em]        }
[size=1em]         
[size=1em]        // Update all steering stuff
[size=1em]        mBoid.update();
[size=1em]    }
[size=1em]}





如果运动员是由人工智能控制的,mBrain会被更新,这会调用当前状态的方法,使得运动员做出相应的行为。如果运动员是由玩家控制的,mBrain会一直被忽略,运动员会按着玩家的操作来移动。
关于可以压入堆栈的状态,目前我们只实现了两个。一个状态是让运动员为比赛做准备。当运动员为比赛做准备的时候,会往冰球比赛场中他自己的位置移动并且到了以后会站着不动,盯着冰球。另外一个状态只是让运动员站着不动盯着冰球。
在下面的部分我们将实现这些状态。
空闲状态
如果运动员是处于空闲状态,他将停止移动并且看着冰球。这个状态是用在运动员已经在冰球比赛中就位了只是在等待某些事情的发生,比如比赛开始。
这个状态会写在Athlete类里的idle()方法里:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em][size=1em]public class Athlete
[size=1em]{   
[size=1em]    // (...)
[size=1em]    public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) {
[size=1em]        // (...)
[size=1em]         
[size=1em]        // Tell the brain the current state is 'idle'
[size=1em]        mBrain.pushState(idle);
[size=1em]    }
[size=1em]      
[size=1em]    private function idle() :void {
[size=1em]        var aPuck :Puck = getPuck();
[size=1em]        stopAndlookAt(aPuck.position);
[size=1em]    }
[size=1em]      
[size=1em]    private function stopAndlookAt(thePoint :Vector3D) :void {
[size=1em]        mBoid.velocity = thePoint - mBoid.position;
[size=1em]        mBoid.velocity = normalize(mBoid.velocity) * 0.01;
[size=1em]    }
[size=1em]}





因为这个方法不会把自己从堆栈中弹出来,它将始终保持活跃状态,在以后,这个状态会把自己弹出来以便给其他状态腾地方,比如攻击状态,但是现在它还不会这么做。
stopAndStareAt()方法遵循的原则与冰球被击中后速度的计算方法是一致的。通过thePoint - mBoid.position来计算运动员位置指向冰球位置的向量,并且用作运动员新的速度向量。
新的速度向量会把运动员往冰球那移动,要确保运动员不动,速度向量要用0.01进行放缩,这会把速度向量的长度变为0。这会让运动员停止移动,但是让他始终看着冰球。
准备比赛状态
如果运动员在准备比赛状态,他将往他在冰球比赛场的初始位置移动,并且平滑的停在那个位置上。在比赛开始前运动员应该站到正确的初始位置上。因为运动员会最终停在初始位置上,所以到达行为可以被再次使用。
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em]22

[size=1em]23

[size=1em]24

[size=1em]25

[size=1em]26

[size=1em][size=1em]public class Athlete
[size=1em]{   
[size=1em]    // (...)
[size=1em]    private var mInitialPosition :Vector3D; // the position in the rink where the athlete should be placed
[size=1em]      
[size=1em]    public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) {
[size=1em]        // (...)
[size=1em]        mInitialPosition = new Vector3D(thePosX, thePosY);
[size=1em]         
[size=1em]        // Tell the brain the current state is 'idle'
[size=1em]        mBrain.pushState(idle);
[size=1em]    }
[size=1em]      
[size=1em]    private function prepareForMatch() :void {
[size=1em]        mBoid.steering = mBoid.steering + mBoid.arrive(mInitialPosition, 80);
[size=1em]         
[size=1em]        // Am I at the initial position?
[size=1em]        if (distance(mBoid.position, mInitialPosition) <= 5) {
[size=1em]            // I'm in position, time to stare at the puck.
[size=1em]            mBrain.popState();
[size=1em]            mBrain.pushState(idle);
[size=1em]        }
[size=1em]    }
[size=1em]      
[size=1em]    // (...)
[size=1em]}




这个状态会用到达行为来让运动员移动到比赛前的初始位置。如果运动员的当前位置和他的初始位置之间的距离小于5,这意味着运动员已经到了要到达的区域。当这种情况出现的时候,准备比赛状态会把自己从堆栈中弹出来并且推入空闲状态,从而让运动员进入新的人工智能状态。
下面是使用基于堆栈的有限状态机控制几个运动员的效果。按G会把它们随机分布在冰球比赛场,进入准备比赛状态:
总结
这篇文章提供了用群体行为和基于堆栈的有限自动机来实现冰球游戏人工智能框架的基础概念。综合使用上述概念,运动员能够在冰球比赛场移动,跟随鼠标光标的位置。运动员还可以击打冰球朝着某个方向前进。
通过使用两个状态和基于堆栈的有限状态机,运动员可以被重置并且往他们在冰球比赛场的初始位置移动,为比赛做准备。
在下篇文章里面,你将学到如何让运动员发起攻击,带着冰球往对方球门移动的同时避开对手。
参考文献:

+1
1973°C
3
  • frankwswswws
  • Hesam1353
  • 初末
过: 他们
因分享而快乐,学习以自强!
frankwswswws 发表于 2018-1-2 10:20:17 显示全部楼层
深绿色的代码背景看着好费眼睛
因分享而快乐,学习以自强!
Hesam1353 发表于 2018-1-6 19:05:25 显示全部楼层
深绿色的代码背景看着好费眼睛
因分享而快乐,学习以自强!
初末
 楼主|
发表于 2018-1-8 11:25:43 显示全部楼层
Hesam1353 发表于 2018-1-6 19:05
深绿色的代码背景看着好费眼睛

好哒,下次注意
来自安卓客户端来自安卓客户端
因分享而快乐,学习以自强!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

VR/AR版块|Unity3d|Unreal4|新手报道|小黑屋|站点地图|沪ICP备14023207号-9|【泰斗社区】-专注互联网游戏和应用的开发者平台 ( 沪ICP备14023207号-9 )|网站地图

© 2001-2013 Comsenz Inc.  Powered by Discuz! X3.4

1
QQ