公司要开始做小游戏了,经过研究讨论之后决定采用Laya作为开发引擎,本身是做Unity3D开发的,学习成本很低,Laya的编程方式和Unity很相似,对Unity开发人员来说没有什么难度,从这篇文章开始就记录一下学习以Laya制作demo。
使用Unity导出场景资源
前去Laya官网下载对应版本的Unity导出插件,在Unity中搭建好场景,并一键导出。(注意Laya并不支持Unity的材质,需要将Unity的材质转换成Laya的材质)
Laya 的资源一般存放在工程路径下bin/res Unity导出的场景文件也放在这个场景即可。
加载Scene场景
Laya的开发方式和Unity一致,因此创建一个GameStart脚本加载3D场景,并将3D场景添加至舞台。因为舞台上存在着一些UI,因此我们需要将3D场景加载到第0号节点,以免3D场景遮挡了其它的UI
onAwake(): void{
Laya3D.init(0, 0);
Laya.Scene3D.load("res/LayaScene_Main/Conventional/Main.ls",Laya.Handler.create(this,this.onSceneLoadComplete));
Laya.stage.on(AppConst.MoveColumn,this,this.moveColumn);
Laya.stage.on(AppConst.CreatePrticle,this,this.onSpawnPrticle)
Laya.stage.on(AppConst.RePlayGame,this,this.onRePlayGame)
}
onSceneLoadComplete(scene):void
{
//加载完成获取到了Scene3d
Laya.stage.addChildAt(scene,0);
this.scene=scene;
//加载完成获取到了Scene3d
Laya.stage.addChildAt(scene,0);
this.scene=scene;
//获取摄像机
var camera = scene.getChildByName("Main Camera");
//清除摄像机的标记
camera.clearFlag = Laya.BaseCamera.CLEARFLAG_SKY;
camera.addComponent(CameraFollow)
var parent=scene.getChildByName("Parent")
this.column=parent.getChildByName("Column")
parent.addComponent(Rotation);
var player=scene.getChildByName("Player")
//拖尾特效
var trail=scene.getChildByName("TrailRender")
player.addChild(trail);
trail.transform.localPosition=new Laya.Vector3();//默认为(0,0,0)
player.addComponent(PlayerController)
var platform=parent.getChildByName("Platform");
platform.active=false
this.owner.addComponent(SpawnPlatform).init(platform,parent);
var particle=scene.getChildByName("Particle")
this.particle=Laya.Sprite3D.instantiate(particle,scene);
}
让柱子跟随手指转动起来
实现让圆柱跟随拖拽旋转逻辑很简单,只需要捕获鼠标按下、拖动以及抬起三个事件,做相应的逻辑处理即可。
注意 Laya.stage.on(Laya.Event.MOUSE_OUT,this,this.onStageMouseUp)
在舞台外鼠标抬起事件无法通过onStageMouseUp
获取,因此需要监听全局的鼠标抬起事件。
import AppConst from "./AppConst";
export default class Rotation extends Laya.Script {
lastMouseX:any
isMouseDown:boolean
isGameOver:boolean
constructor(){
super();
this.lastMouseX=0;
this.isMouseDown=false
this.isGameOver=false;
}
/*组件被激活后执行,此时所有节点和组件均已创建完毕,次方法只执行一次*/
onAwake(): void{
//鼠标移出后需要处理为在舞台上抬起鼠标,
Laya.stage.on(Laya.Event.MOUSE_OUT,this,this.onStageMouseUp)
Laya.stage.on(AppConst.GameOver,this,this.onGameOver);
Laya.stage.on(AppConst.RePlayGame,this,this.onRePlayGame)
}
onRePlayGame():void
{
this.isGameOver=false;
(this.owner as Laya.Sprite3D).transform.localRotationEulerY=0;
}
onGameOver():void
{
this.isGameOver=true;
this.isMouseDown=false;
}
onStageMouseDown(e):void
{
if(this.isGameOver)
return;
this.lastMouseX=e.stageX;//鼠标在舞台上X轴位置
this.isMouseDown=true
}
onStageMouseMove(e):void
{
if(this.isGameOver)
return;
if(this.isMouseDown)
{
var deltaX=e.stageX-this.lastMouseX;
(this.owner as Laya.Sprite3D).transform.rotate(new Laya.Vector3(0,deltaX/5,0),true,false);
this.lastMouseX=e.stageX;
}
}
onStageMouseUp(e):void
{
if(this.isGameOver)
return;
this.lastMouseX=0;
this.isMouseDown=false;
}
}
小球下落以及平台生成
- Laya提供了角色控制器,Laya.CharacterController 给小球添加角色控制器和碰撞体,通过剩下碰撞来触发穿过,碰到平台,和触发死亡等事件。不建议使用onCollisionEnter和onTriggerExit(other)小球可能同时碰到普通平台或者死亡区域,使用射线碰撞可以避免这个问题。同时为了避免连续掉落后直接碰到死亡区域,在连续通过三次平台后就将平台设置为无敌模式。每通过一个平台积分+1.
import AppConst from "./AppConst";
export default class PlayerController extends Laya.Script3D {
constructor(){ super(); }
player:Laya.CharacterController;
physics:Laya.PhysicsSimulation;
ray:Laya.Ray;
hitResult:Laya.HitResult
isGameOver:boolean;
throughCount:number;
/*组件被激活后执行,此时所有节点和组件均已创建完毕,次方法只执行一次*/
onAwake(): void{
//获取场景物理模拟器用于射线碰撞
this.physics=(this.owner.parent as Laya.Scene3D).physicsSimulation;
//创建射线
this.ray=new Laya.Ray(new Laya.Vector3(),new Laya.Vector3(0,-1,0));
this.hitResult=new Laya.HitResult();
//穿件角色控制
this.player=this.owner.addComponent(Laya.CharacterController);
var collider=new Laya.SphereColliderShape(0.2)
this.player.colliderShape=collider;
this.player.fallSpeed=50;//下落速度
this.player.jumpSpeed=7;//弹回速度
this.isGameOver=false;
this.throughCount=0;
Laya.timer.frameLoop(1,this,this.RayCast)
Laya.stage.on(AppConst.RePlayGame,this,this.onRePlayGame)
}
onRePlayGame():void
{
(this.owner as Laya.Sprite3D).transform.localPositionY=0;
this.isGameOver=false;
}
RayCast(): void {
if(this.isGameOver)
return;
this.ray.origin=(this.owner as Laya.MeshSprite3D).transform.position;
if(this.throughCount>=3&&this.physics.rayCast(this.ray,this.hitResult,0.2))
{
var col=this.hitResult.collider as Laya.PhysicsCollider
this.setPlatform(col.owner.parent)
}
if(this.physics.rayCast(this.ray,this.hitResult,0.15))
{
var col=this.hitResult.collider as Laya.PhysicsCollider
if(col.owner.name=="Obstacle")
{
this.throughCount=0;
this.isGameOver=true;
console.log("GameOver",col.owner.parent)
Laya.stage.event(AppConst.GameOver)
return;
}
else if(col.owner.name=="Bar")
{
// 碰到平台,播放一个粒子特效
Laya.stage.event(AppConst.CreatePrticle,(col.owner as Laya.Sprite3D).transform.position);
this.player.jump();
this.throughCount=0;
}
else
{
col.owner.parent.removeSelf();
//回收
Laya.Pool.recover("Platform",col.owner.parent)
Laya.stage.event(AppConst.MoveCamera,[(col.owner.parent as Laya.Sprite3D).transform.localPositionY,col.owner.parent])
//生成一个平台
Laya.stage.event(AppConst.CreatePlatform);
Laya.stage.event(AppConst.MoveColumn);
this.throughCount++;//计算通过个数,生成无敌模式
if(this.throughCount>=3)
{
Laya.stage.event(AppConst.AddScore,3);
}
else
{
Laya.stage.event(AppConst.AddScore,1);
}
}
}
}
setPlatform(platform):void
{
//第一个平台需要特殊处理
for(var i=0;i<platform.numChildren;i++)
{
var child=platform.getChildAt(i)
child.meshRenderer.material._ColorR=0;
child.meshRenderer.material._ColorG=0;
child.meshRenderer.material._ColorB=1;
if(child.name!="Empty")
child.name="Bar";
}
}
}
- 小球每穿过一个平台就将当前平台销毁,并在下方重新生成一个平台。用“Bar”代表普通平台,用“Obstacle”代表死亡区域,用“Empty”代表可以穿过的区域
import AppConst from "./AppConst";
export default class SpawnPlatform extends Laya.Script {
initPosY:any
spawnCount:any
prefab:Laya.Sprite3D
parent:Laya.Sprite3D
platformArr:Laya.Sprite3D[]=[];
constructor(){ super();
this.initPosY=-1;
this.spawnCount=0
}
init(prefab,parent):void{
this.prefab=prefab;
this.parent=parent;
this.initPosY=-1;
this.spawnCount=0
for(var i=0;i<10;i++)
{
this.spawn(prefab,parent)
}
Laya.stage.on(AppConst.CreatePlatform,this,this.spawn)
Laya.stage.on(AppConst.RePlayGame,this,this.onRePlayGame)
}
onRePlayGame():void
{
this.platformArr.forEach(element => {
if(element.displayedInStage)
{
element.removeSelf();
Laya.Pool.recover("Platform",element)
}
});
this.init(this.prefab,this.parent);
}
spawn(prefab,parent):void
{
//Laya 对象池
var temp=Laya.Pool.getItemByCreateFun("Platform",this.createFun,this)
//var temp=Laya.Sprite3D.instantiate(prefab,parent)
this.parent.addChild(temp)
temp.active=true
temp.transform.localPosition=new Laya.Vector3(0,this.initPosY-(1.5*this.spawnCount),0);
this.setPlatform(temp);
this.platformArr.push(temp);
this.spawnCount++;
}
//如果对象池没有缓存对象,则调用此方法创建
createFun():any
{
var temp=Laya.Sprite3D.instantiate(this.prefab,this.parent);
return temp
}
//随机设置平台的空位置
setPlatform(platform):void
{
//还原为默认初始状态
if(this.spawnCount==0)
{
//第一个平台需要特殊处理
for(var i=0;i<platform.numChildren;i++)
{
var child=platform.getChildAt(i)
child.getComponent(Laya.PhysicsCollider).isTrigger=false;
child.meshRenderer.material._ColorR=0;
child.meshRenderer.material._ColorG=0;
child.meshRenderer.material._ColorB=0;
child.meshRenderer.enable=true
child.name="Bar"
if(i==0||i==1)
{
child.name="Empty"
child.meshRenderer.enable=false
//child.getComponent(Laya.PhysicsCollider).isTrigger=true;
}
else
{
child.meshRenderer.enable=true;
child.getComponent(Laya.PhysicsCollider).isTrigger=false;
}
}
return;
}
var emptyCount=0;
var obstacleValue=Math.floor(this.getRandom(0,platform.numChildren-1))
for(var i=0;i<platform.numChildren;i++)
{
var child=platform.getChildAt(i)
//因为有缓存池的存在,因此需要重置
child.getComponent(Laya.PhysicsCollider).isTrigger=false;
child.meshRenderer.material._ColorR=0;
child.meshRenderer.material._ColorG=0;
child.meshRenderer.material._ColorB=0;
child.meshRenderer.enable=true
child.name="Bar"
//控制空位置不超过4个
if(obstacleValue!=i&&emptyCount<3&&this.getRandom(0,10)>8)
{
child.meshRenderer.enable=false;
//child.getComponent(Laya.PhysicsCollider).isTrigger=true;
child.name="Empty";
emptyCount++;
}
else if(obstacleValue==i)
{
child.meshRenderer.material._ColorR=1;
child.meshRenderer.material._ColorG=0;
child.meshRenderer.material._ColorB=0;
child.name="Obstacle"
}
}
if(emptyCount==0)
{
//
var child=platform.getChildAt(0)
child.name="Empty"
child.meshRenderer.material._ColorR=0;
child.meshRenderer.material._ColorG=0;
child.meshRenderer.material._ColorB=0;
child.meshRenderer.enable=false
// child.getComponent(Laya.PhysicsCollider).isTrigger=true;
}
}
getRandom(min,max){
var delta=max-min;
var randomValue=Math.random()*delta
return min+randomValue
}
摄像机跟随与圆柱下移
当小球每次下落之后我们需要将摄像机和圆柱跟随移动,避免穿帮。Laya也很贴心的封装了类似DOTween的功能插件。
//相机移动
onCameraFollw(posY,obj):void
{
Laya.Tween.to( (this.owner as Laya.Camera).transform,{localPositionY:posY},700)
}
//柱子
moveColumn(obj):void
{
(this.column as Laya.Sprite3D).transform.localPositionY-=1.5;
}
Laya源码工程以上传,可下载查看
写在后面的话
对于本身从事Unity开发的人员来说,学习用Laya来做小游戏上手很快,但是也有很多坑需要趟。例如在此demo中,如果采用Laya提供的onUpdate方法中进行射线检测会出现直接穿过平台而不销毁(射线碰撞没有检测到),而采用Laya提供的帧函数方法Laya.timer.frameLoop(1,this,this.RayCast)
则不会出现,目测是onUpdate可能没有完全的按照每一帧来刷新,需要学习做更多的项目,熟悉度高了才更快的迭代项目。当量变达到一定程度后也会发生质变。