菜鸟教程 | egret EUI卡牌游戏制作

写在前面

  随着越来越多的新人开始接触白鹭引擎,创作属于自己的游戏。考虑到初学者会遇到的一些实际操作问题,我们近期整理推出《菜鸟教程》系列的文档,以便更好的让这些开打着们快速上手,Egret大神们可以忽略此类内容。本文作者是我们技术支持部门的同事“熊猫少女”。看文的小伙伴如果有问题可以来白鹭官方论坛与之交流。

  EUI是一套基于Egret核心显示列表的UI扩展库,它封装了大量的常用UI组件,能够满足大部分的交互界面需求,即使更加复杂的组件需求,您也可以基于EUI已有组件进行组合或扩展,从而快速实现需求。为了展示EUI的功能,我借助白鹭官网中的一个卡牌游戏DEMO,从零开始编写完成这个DEMO。其实这并不是一个完整的游戏,只是一个演示DEMO,亲自实现这个DEMO之后,应该能熟悉大部分的EUI操作,并且增加对egret游戏开发的理解。

演示地址

点进去之后可以体验一下,把所有地方都点一点,感受一下所有的功能,接下来我们就要自己去动手实现了。(这里可以下载源码素材)不过这里的源码是编译之后的并不是源代码,不过我们只需要用到里面的素材,具体实现就让我一步一步自己来吧~~~

初始化

新建好EUI项目(480 * 800),把刚刚下载的文件里面的 source > resource > art 这个文件夹放在我们自己新建的项目中

按照之前的流程,现在删除新建的项目中多余的代码

上面还有个函数的调用要记得一起删掉

现在项目干净了~

Let‘s do it

场景搭建

EUI最方便的地方就是能快速的搭建界面,只需要用鼠标拖动就可以搭建静态的界面。先来把游戏的素材放到加载组里

打开default.res.json

新建两个资源组分别放hero_goods和loading,其他的都放到preload组就好

现在稍微分析一下

这个游戏一共有五个场景:

主场景

玩家场景

英雄场景

物品场景

关于场景

loading

在主场景之前,其实还有一个loading需要先显示出来,这样用户就不会看到一片黑乎乎的加载过程,提高用户体验

根据我们之前设置的几个资源组,preload是游戏初始化需要加载的资源,我们的loading则需要在它之前就加载好,这样用户就能先看到loading页,so现在去写个loading

项目其实自带了一个很简陋的loading,我们可以再这个基础上来写

打开main.ts

先加载这个资源组里面的资源,然后再把loadingView添加到舞台

打开loadingUi.ts

class LoadingUI extends egret.Sprite implements RES.PromiseTaskReporter {

   public constructor() {
       super();

       // 当被添加到舞台的时候触发 (被添加到舞台,说明资源组已经加载完成)
       this.addEventListener(egret.Event.ADDED_TO_STAGE, this.createView, this)
  }

   private textField: egret.TextField; // 文本
   private bgImg: egret.Bitmap // 背景图
   private loadImg: egret.Bitmap // loading图标

   private createView(): void {
       this.width = this.stage.stageWidth
       this.height = this.stage.stageHeight

       // 背景图
       this.bgImg = new egret.Bitmap()
       this.bgImg.texture = RES.getRes(‘loading_jpg‘)
       this.addChild(this.bgImg)

       // loading图标
       this.loadImg = new egret.Bitmap()
       this.loadImg.texture = RES.getRes(‘loading2_png‘)
       this.loadImg.anchorOffsetX = this.loadImg.width / 2
       this.loadImg.anchorOffsetY = this.loadImg.height / 2
       this.loadImg.x = this.width / 2
       this.loadImg.y = this.height / 2
       this.addChild(this.loadImg)

       // 文本
       this.textField = new egret.TextField();
       this.addChild(this.textField);
       this.textField.width = 480;
       this.textField.height = 20
       this.textField.y = this.height / 2 - this.textField.height / 2;
       this.textField.size = 14
       this.textField.textAlign = "center";

       // 监听帧事件,每帧都让loading图片转动
       this.addEventListener(egret.Event.ENTER_FRAME, this.updata, this)
  }
   private updata() {
       // 旋转
       this.loadImg.rotation += 5
  }
   // 这个函数在加载中会自动调用
   public onProgress(current: number, total: number): void {
       // 计算百分比
       let per = Math.floor((current / total) * 100)
       this.textField.text = `${per}%`;
  }
}

loading实现完成~现在调试看看,在终端输入 egret run -a(-a 表示修改代码保存后自动编译,你只需要在浏览器刷新就可以看到修改后的效果)

能看到loading页面了~

主场景

主场景就是我们进入游戏的时候看到的第一个场景,其他的四个场景就是在点击下面不同的按钮的时候添加到当前的主场景上就好啦~

首先我们来把几个场景的组件搞定。

  1. 1. 在resource目录下新建一个skins目录,用来存放我们创建的皮肤
  2. 2. 在scr目录下单击右键,新建一个EUI组件

创建主场景

相对于我们之前先创建exml再创建对应的ts文件要方便很多,而且这样不需要在ts文件中指定skinName,因为直接创建EUI组件的时候它在配置文件中已经帮你指定好了。

可以在default.thm.json文件里面看到

现在点开自动生成的MainScene.exml

设置好宽高 480 * 800

把图片拖进来,然后再快捷约束里面点击左对齐和上对齐,图片就自动调整好了

现在需要去设置场景下方的四个按钮

每个按钮都有两种状态,正常和按下。 这里需要单独去新建单个按钮的皮肤,下面以玩家按钮为例

新建mbtnPlayer.exml ,设置宽高 111 * 80点击下面状态旁边的 + 号,给这个皮肤设置不同的状态up是正常状态, down是按下状态, disabled是禁用状态这里我们先设置up状态

在up状态中,把正常状态的背景图和按钮图片拖进来。

同理,把按下状态也搞定。(记得按下状态的背景图不一样)

现在我们得到了一个自定义的组件皮肤,回到 MainScene.exml,为了更好管理四个按钮,所以先拖进来一个Group,并给他一个id Group_mbtn

给这个Group设置好布局,一会儿里面的按钮就会自动排列好,不用手动去拖动

往Group中放置一个 ToggleButton ,把皮肤换成我们刚刚自定义的mbtnPlayer

照葫芦画瓢,把其他三个都搞定吧~

主场景搭建完毕

做完了主场景,现在开始写一些关于主场景的逻辑

上面忘了说要记得给那几个按钮设置id

打开 MainScene.ts

class MainScene extends EUI.Component implements  EUI.UIComponent {

public Group_mbtn:EUI.Group;

public mgtnPlayer:EUI.ToggleButton;

public mbtnHero:EUI.ToggleButton;

public mbtnGoods:EUI.ToggleButton;

public mbtnAbout:EUI.ToggleButton;

public constructor() {

super();

}

protected partAdded(partName:string,instance:any):void

{

super.partAdded(partName,instance);

}

protected childrenCreated():void

{

super.childrenCreated();

// 让Group可以点击

this.Group_mbtn.touchEnabled = true

// 事件委托, 点击按钮的时候触发toggleBtn

this.Group_mbtn.addEventListener(egret.TouchEvent.TOUCH_TAP, (e)=> {

let theBtn = <EUI.ToggleButton>e.target

// 在点击触发这个事件的时候,点击的那个btn已经变成了选中状态

// 判断theBtn是否存在theBtn.selected属性且为true

if (theBtn.selected && theBtn.selected != undefined) {

this.toggleBtn(theBtn)

} else {

// 当selected为false的时候,说明按钮在点击之前就是选中状态

// 点击后变成了false,所以这里改回选中状态

theBtn.selected = true

}

}, this)

}

/**

* 切换按钮

*/

public toggleBtn(btn:EUI.ToggleButton) {

// 先把所有的按钮都设置为不选中

for (let i = 0; i < this.Group_mbtn.numChildren; i++) {

let theBtn = <EUI.ToggleButton>this.Group_mbtn.getChildAt(i)

theBtn.selected = false

}

// 把传进来的btn设置为选中状态

btn.selected = true

}

}
在Main.ts里,把主场景添加到舞台
protected createGameScene(): void {

       this.addChild(new MainScene())

  }

现在运行就可以看到切换按钮的效果

玩家场景

现在开始创建 玩家场景 PlayerScene 组件

设置宽高 680*800, 拖拽图片和button

注意红色框的是button

这三个按钮在点击的时候会变大,这个效果我们可以在制作皮肤的时候就完成

首先用鼠标点中返回按钮,然后点击左上方的 源码

这一段就是那个返回按钮的源码,修改成下图

width.down = "100%" 表示当按钮按下的时候宽度为 100%,其他情况下宽度90%

horizontalCenter="0" verticalCenter="0" 表示让图片以中心放大

可以看到红框部分是一个可以拖动的窗口,所以我们需要放置一个 scroller

把scroller里面自带的group删掉,加上一个数据容器list

现在创建list里面的某一个子项的皮肤 zhuangbeiItem.exml

宽高设为 87*130 ,拖入一张图片和一个labl控件还有一个image控件,设置好label的样式

这个皮肤里面的label和image控件的值都是需要我们去提供的

需要在标签里面写 {data.xx} 其实很像js里面某些框架的插值写法

给label 的标签写上 {data.name} , 给image 的资源名写上 {data.image}

装备item完成,回到刚刚的PLayerScene, 把list的条目皮肤设置为刚刚创建的装备item

把list布局设置成水平布局

给scroller和list都取个id,便于后面使用

这个scroller只需要左右拖动,所以我们去 ‘所有属性’ 打开它的水平滚动,关掉垂直滚动

打开PLayerScene.ts

class PlayerScene extends EUI.Component implements  EUI.UIComponent {

public btn_return:EUI.Button;

public btn_zhuangbei:EUI.Button;

public btn_qianghua:EUI.Button;

public scr_zhuangbei:EUI.Scroller;

public list_zhuangbei:EUI.List;

public constructor() {

super();

}

protected partAdded(partName:string,instance:any):void

{

super.partAdded(partName,instance);

}

protected childrenCreated():void

{

super.childrenCreated();

// 数组数据

let dataArr:any[] = [

{image:"resource/art/profile/skillIcon01.png",name:"旋龙幻杀"},

{image:"resource/art/profile/skillIcon02.png",name:"魔魂天咒"},

{image:"resource/art/profile/skillIcon03.png",name:"天魔舞"},

{image:"resource/art/profile/skillIcon04.png",name:"痴情咒"},

{image:"resource/art/profile/skillIcon05.png",name:"无间寂"},

{image:"resource/art/profile/skillIcon06.png",name:"霸天戮杀"},

{image:"resource/art/profile/skillIcon07.png",name:"灭魂狂飙"}

]

// 把数组数据转成EUI数组

let EUIArr:EUI.ArrayCollection = new EUI.ArrayCollection(dataArr)

// 把EUI数组作为list的数据源

this.list_zhuangbei.dataProvider = EUIArr

// 隐藏进度条

this.scr_zhuangbei.horizontalScrollBar.autoVisibility = false

}

}

到这里,  玩家场景也创建完成啦~

场景管理类

我们有了两个场景,可以来做场景之间的切换了

还记得之前说的思路吗,舞台上先有一个主场景,然后点击不同按钮的时候把对应的场景添加到主场景上

这里有个需要注意的地方,子场景添加进来默认层级是高于主场景的,所以会把主场景给挡住了,而我们需要点击主场景的按钮。所以我们需要把主场景中放置按钮的Group的层级提高。

我用一个场景管理类来管理这些场景

新建一个SceneManager.ts,采用的是单例模式,要使用这个类的时候不要new SceneManager实例,而是用 SceneManager.instance 来获取到这个类的实例

这样可以保证场景管理类有且只有一个实例,便于管理操作

(使用 static修饰的方法都是静态方法,简单的说就是调用的时候不是通过实例调用,而是直接用类名来调用, 类名.方法名)

下面是一个基础的场景管理类,现在来逐步完善它的功能

/**

* 场景管理类

*/

class SceneManager {

   private _stage:egret.DisplayObjectContainer // 设置所有场景所在的舞台(根)

   private mainScene:MainScene //主场景

   private playerScene:PlayerScene //玩家场景

   // 在构造函数中创建好场景,保存到属性

   constructor() {

       this.mainScene = new MainScene()

       this.playerScene = new PlayerScene()

  }

   /**

    * 获取实例

    */

   static sceneManager:SceneManager

   static get instance(){

       if(!this.sceneManager) {

           this.sceneManager =  new SceneManager()

      } 

       return this.sceneManager

  }

   /**

    * 设置根场景

    */

   public setStage(s:egret.DisplayObjectContainer) {

       this._stage = s

  }

   // 这里补充代码……

}

首先需要管理的场景是主场景,SceneManager.instance.mainScene是获取到主场景的实例

SceneManager.instance 获取到场景管理类的实例

然后再.mainScene 获取到构造方法constructor中的 this.mainScene = new MainScene() 得到的主场景 的实例

 /**

    * 主场景

    */

   static toMainScene() {

       let stage:egret.DisplayObjectContainer = this.instance._stage // (根) 舞台

       let mainScene = SceneManager.instance.mainScene // 主场景

       // 判断主场景是否有父级(如果有,说明已经被添加到了场景中)

       if(!mainScene.parent){

           // 未被添加到场景

           // 把主场景添加到之前设置好的根舞台中

           stage.addChild(mainScene)

      } 

  }

现在到main.ts中使用场景管理类来加载主场景

/**

    * 创建场景界面

    * Create scene interface

    */

   protected createGameScene(): void {

       // 把this设置为场景管理器的根舞台

       SceneManager.instance.setStage(this)

       // 调用SceneManager的静态方法

       SceneManager.toMainScene()

  }

现在运行,可以看到主场景已经被添加到舞台中了

现在只有一个场景,需要把第二个场景也加进来,并实现切换

在场景管理类中再添加一个静态方法

/**

    * 玩家场景

    */

   static toPlayerScene() {

       let stage:egret.DisplayObjectContainer = this.instance._stage

       // 把玩家场景添加到主场景中

       this.instance.mainScene.addChild(this.instance.playerScene)

  }

当在主场景点击玩家按钮的时候,调用这个方法,切换到玩家场景

这里需要稍微改动一下之前的点击函数

打开MainScene.ts

     /**

* 切换按钮

* @param btn 参数是EUI.ToggleButton的时候切换按钮, 参数是0的时候设置为全部不选中

*/

public toggleBtn(btn:EUI.ToggleButton | number) {

// 先把所有的按钮都设置为不选中

for (let i = 0; i < this.Group_mbtn.numChildren; i++) {

let theBtn = <EUI.ToggleButton>this.Group_mbtn.getChildAt(i)

theBtn.selected = false

}

if(btn===0){

return

}

// 把传进来的btn设置为选中状态

btn = <EUI.ToggleButton>btn

btn.selected = true

// 获取当前点击的按钮的下标, 用来实现不同按钮对应的功能

// 0 1 2 3 对应 玩家, 英雄, 物品, 关于

let index = this.Group_mbtn.getChildIndex(<EUI.ToggleButton>btn)

switch (index) {

case 0:

// 调用静态方法切换到玩家场景

SceneManager.toPlayerScene()

// 把按钮的层级提高

// this.numChildren表示所有的子元素数量

this.setChildIndex(this.Group_mbtn, this.numChildren)

break

default:

break

}

}

点击玩家按钮就可以正常切换到玩家场景了,现在来实现其中的返回按钮

点击返回按钮,回到主场景,并且下面的按钮全都变成未选中状态

打开PlayerScene.ts

// 给返回按钮添加事件

this.btn_return.addEventListener(egret.TouchEvent.TOUCH_TAP, this.returnMain, this)

/**

* 回到主场景

*/

private returnMain() {

SceneManager.toMainScene()

}

点击按钮跳转回到主场景,其实就是删除掉当前覆盖在主场景上的玩家场景,主场景就能显示出来了

so,打开SceneManager.ts,完善一下刚刚的函数

/**

    * 主场景

    */

   static toMainScene() {

       let stage:egret.DisplayObjectContainer = this.instance._stage // (根) 舞台

       let mainScene = SceneManager.instance.mainScene // 主场景

       // 取消所有按钮的选中状态

       mainScene.toggleBtn(0)

       // 判断主场景是否有父级(如果有,说明已经被添加到了场景中)

       if(!mainScene.parent){

           // 未被添加到场景

           // 把主场景添加到之前设置好的根舞台中

           stage.addChild(mainScene)

      } 

       // 判断玩家场景是否有父级(是否在场景中)

       if(SceneManager.instance.playerScene.parent) {

           // 如果有就删除玩家场景

           mainScene.removeChild(SceneManager.instance.playerScene)

      }

  }

保存文件

去浏览器里就能看到效果了~

英雄场景

开始制作英雄场景 HeroScene.exml

到源码部分,修改一下两个按钮的效果

在中间放置一个Scroller,然后里面放一个List,

跟之前玩家场景其实差不多啦,现在去创建 heroListItem.exml

这里需要注意,有个checkBox,用来选中某个list

把里面的数据插值写好 {data.image} {data.name} {data.value}

checkBox是个例外,它的值不能用{data.xx}的方式来指定,我们需要创建一个单独的类

Herolist_item.ts

// 必须要继承自 EUI.ItemRenderer

class HeroList_item extends EUI.ItemRenderer{

// 选择框

public ce_select:EUI.CheckBox;

public constructor() {

super()

// 把这个 类和皮肤 联系起来

this.skinName = ‘resource/skins/skins_item/heroListItem.exml‘

}

       // 当数据改变时,更新视图

protected dataChanged() {

// isSeleted 是我们提供数据的某个字段

this.ce_select.selected = this.data.isSelected

}

}

回到HeroScene.exml, 把list的条目皮肤设置为heroListItem,并给他们设置好id

在所有属性里面把水平滚动关掉,垂直滚动打开

打开英雄场景HeroScene.ts

class HeroScene extends EUI.Component implements  EUI.UIComponent {

public btn_return:EUI.Button;

public btn_select:EUI.Button;

public scr_hero:EUI.Scroller;

public list_hero:EUI.List;

public constructor() {

super();

}

protected partAdded(partName:string,instance:any):void

{

super.partAdded(partName,instance);

}

protected childrenCreated():void

{

super.childrenCreated();

// 原始数组

let dataArr:any[] = [

{image: ‘resource/art/heros_goods/heros01.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false},

{image: ‘resource/art/heros_goods/heros02.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false},

{image: ‘resource/art/heros_goods/heros03.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: true},

{image: ‘resource/art/heros_goods/heros04.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false},

{image: ‘resource/art/heros_goods/heros05.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false},

{image: ‘resource/art/heros_goods/heros06.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false},

{image: ‘resource/art/heros_goods/heros07.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false}

]

// 转成EUI数组

let EUIArr:EUI.ArrayCollection = new EUI.ArrayCollection(dataArr)

// 把list_hero数据源设置成EUIArr

this.list_hero.dataProvider = EUIArr

// 设置list_hero的项呈视器 (这里直接写类名,而不是写实例)

this.list_hero.itemRenderer = HeroList_item

}

}

现在运行一下,能看到列表已经能正确的加载,而且数组中isSeleted字段为true的第三项也被默认选中了。

现在来实现手动更改列表的选中状态

Herolist_item.ts

// 必须要继承自 EUI.ItemRenderer

class HeroList_item extends EUI.ItemRenderer{

// 选择框

public ce_select:EUI.CheckBox;

public constructor() {

super()

// 把这个 类和皮肤 联系起来

this.skinName = ‘resource/skins/skins_item/heroListItem.exml‘

// 当组件创建完成的时候触发

this.addEventListener(EUI.UIEvent.CREATION_COMPLETE, this.onComplete, this)

}

private onComplete() {

// 当select的选中状态发生改变的时候触发

this.ce_select.addEventListener(EUI.UIEvent.CHANGE, (e) => {

// this.data 就是绑定的数据, 

this.data.isSelected = this.ce_select.selected

// 把数据打印出来看看

console.log(this.data);

}, this)

}

// 当数据改变时,更新视图

protected dataChanged() {

// isSeleted 是我们提供数据的某个字段

this.ce_select.selected = this.data.isSelected

}

}

这里可能稍微有点儿绕,需要花一点时间好好理解一下代码执行的流程。

现在点击checked框的时候就能正确的修改数据的isSelected了

继续完成返回和确定选择的按钮

返回按钮其实就是把场景切换到主场景去,在场景控制类中写个方法就好

确定选择的按钮其实也是要切换回到主场景,再获取一下数据里面isSelected为true的项,并把它们显示到屏幕上

设置返回按钮,直接在点击事件中调用场景管理的方法就好了,把按钮的选择状态都清除

HeroScene.ts

// 点击返回按钮,回到主场景

this.btn_return.addEventListener(egret.TouchEvent.TOUCH_TAP, (e)=>{

SceneManager.toMainScene()

SceneManager.instance.mainScene.toggleBtn(0)

}, this)

选择功能,点击按钮的时候获取到数据源中的isSelected为true的项都保存到数组中,然后把这个数组作为参数传到场景管理里面。拿到数组就创建对应的消息显示出来就好了

HeroScene.ts

// 点击确定按钮,回到主场景,显示出选择的项

this.btn_select.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onClickSelect, this)

        /**

* 点击确定按钮

*/

onClickSelect(e) {

SceneManager.toMainScene()

SceneManager.instance.mainScene.toggleBtn(0)

// 拿到数据源

let dataProvider = this.list_hero.dataProvider

let arr:string[] = []

// 遍历数据源中所有项

for(let i = 0; i < dataProvider.length; i ++) {

let item = dataProvider.getItemAt(i)

if (item.isSelected) {

arr.push(item.name)

}

}

SceneManager.showInfo(arr)

}

SceneManager.ts

   /**

    * 在主场景显示选择的数据

    */

   static showInfo(arr:string[]) {

       let text:string = ‘你选择了: ‘

       if (arr.length === 0) {

           text = ‘厉害了什么都不选‘

      } else {

           text += arr.toString()

      }

       // 新建一个消息背景图

       let img:egret.Bitmap = new egret.Bitmap()

       img.texture = RES.getRes(‘toast-bg_png‘)

       SceneManager.instance.mainScene.addChild(img)

       img.x = SceneManager.instance.mainScene.width / 2 - img.width / 2

       img.y = 500

       img.height = 40

       // 新建一个label用来显示

       let label:egret.TextField = new egret.TextField(); 

       label.text = text

       label.size = 20

       SceneManager.instance.mainScene.addChild(label)

       label.x = SceneManager.instance.mainScene.width / 2 - label.width / 2

       label.y = 510

       label.height = 40

       // 创建一个定时器,1000毫秒后删除label

       let timer:egret.Timer = new egret.Timer(1000, 1)

       timer.start()

       timer.addEventListener(egret.TimerEvent.TIMER_COMPLETE, (e)=>{

           SceneManager.instance.mainScene.removeChild(label)

           SceneManager.instance.mainScene.removeChild(img)

      }, this)

  }

到这里,我们的游戏卡牌项目的重点已经基本完成了。

后面还有两个场景,物品场景,关于场景

物品场景其实就是EUI搭建好场景后,加上一个Scroller,然后里面放上数据列表就好,跟前面的操作都一样

关于场景根本就是添加一个图片到场景就ok

最后

如果你按照之前的几篇文章一步一步的动手实现到了这里,那么你已经完成了

1. EUI项目的创建

2. exml皮肤界面的拖拽搭建

3. EUI基本控件的使用方法: image button scorller list ……

4. loading的实现

5. 游戏场景切换:实现了场景管理类

6. 游戏开发的一些思路,代码组织方式 (虽然我的不一定是对的,但是能参考一下)

你已经很强了,所以剩下的两个场景实现起来肯定毫无问题。

我的代码还有很多可以优化的地方, 比如场景管理里面每次都要判断删除其他场景,  完全可以封装一个方法来使用。

做完这个项目最好再好好看一遍自己写的代码,重点是要明确思路,理清代码逻辑。

源码

 

创建主场景

相对于我们之前先创建exml再创建对应的ts文件要方便很多,而且这样不需要在ts文件中指定skinName,因为直接创建EUI组件的时候它在配置文件中已经帮你指定好了。

可以在default.thm.json文件里面看到

现在点开自动生成的MainScene.exml

设置好宽高 480 * 800

把图片拖进来,然后再快捷约束里面点击左对齐和上对齐,图片就自动调整好了

现在需要去设置场景下方的四个按钮

每个按钮都有两种状态,正常和按下。 这里需要单独去新建单个按钮的皮肤,下面以玩家按钮为例

新建mbtnPlayer.exml ,设置宽高 111 * 80

点击下面状态旁边的 + 号,给这个皮肤设置不同的状态

up是正常状态, down是按下状态, disabled是禁用状态

这里我们先设置up状态

在up状态中,把正常状态的背景图和按钮图片拖进来。

同理,把按下状态也搞定。(记得按下状态的背景图不一样)

现在我们得到了一个自定义的组件皮肤

回到 MainScene.exml

为了更好管理四个按钮,所以先拖进来一个Group,并给他一个id Group_mbtn

给这个Group设置好布局,一会儿里面的按钮就会自动排列好,不用手动去拖动

往Group中放置一个 ToggleButton ,把皮肤换成我们刚刚自定义的mbtnPlayer

照葫芦画瓢,把其他三个都搞定吧~

主场景搭建完毕

做完了主场景,现在开始写一些关于主场景的逻辑

上面忘了说要记得给那几个按钮设置id

打开 MainScene.ts

class MainScene extends EUI.Component implements  EUI.UIComponent {

public Group_mbtn:EUI.Group;

public mgtnPlayer:EUI.ToggleButton;

public mbtnHero:EUI.ToggleButton;

public mbtnGoods:EUI.ToggleButton;

public mbtnAbout:EUI.ToggleButton;

public constructor() {

super();

}

protected partAdded(partName:string,instance:any):void

{

super.partAdded(partName,instance);

}

protected childrenCreated():void

{

super.childrenCreated();

// 让Group可以点击

this.Group_mbtn.touchEnabled = true

// 事件委托, 点击按钮的时候触发toggleBtn

this.Group_mbtn.addEventListener(egret.TouchEvent.TOUCH_TAP, (e)=> {

let theBtn = <EUI.ToggleButton>e.target

// 在点击触发这个事件的时候,点击的那个btn已经变成了选中状态

// 判断theBtn是否存在theBtn.selected属性且为true

if (theBtn.selected && theBtn.selected != undefined) {

this.toggleBtn(theBtn)

} else {

// 当selected为false的时候,说明按钮在点击之前就是选中状态

// 点击后变成了false,所以这里改回选中状态

theBtn.selected = true

}

}, this)

}

/**

* 切换按钮

*/

public toggleBtn(btn:EUI.ToggleButton) {

// 先把所有的按钮都设置为不选中

for (let i = 0; i < this.Group_mbtn.numChildren; i++) {

let theBtn = <EUI.ToggleButton>this.Group_mbtn.getChildAt(i)

theBtn.selected = false

}

// 把传进来的btn设置为选中状态

btn.selected = true

}

}

在Main.ts里,把主场景添加到舞台

protected createGameScene(): void {

       this.addChild(new MainScene())

  }

现在运行就可以看到切换按钮的效果

玩家场景

现在开始创建 玩家场景 PlayerScene 组件

设置宽高 680*800, 拖拽图片和button

注意红色框的是button

这三个按钮在点击的时候会变大,这个效果我们可以在制作皮肤的时候就完成

首先用鼠标点中返回按钮,然后点击左上方的 源码

这一段就是那个返回按钮的源码,修改成下图

width.down = "100%" 表示当按钮按下的时候宽度为 100%,其他情况下宽度90%

horizontalCenter="0" verticalCenter="0" 表示让图片以中心放大

可以看到红框部分是一个可以拖动的窗口,所以我们需要放置一个 scroller

把scroller里面自带的group删掉,加上一个数据容器list

现在创建list里面的某一个子项的皮肤 zhuangbeiItem.exml

宽高设为 87*130 ,拖入一张图片和一个labl控件还有一个image控件,设置好label的样式

这个皮肤里面的label和image控件的值都是需要我们去提供的

需要在标签里面写 {data.xx} 其实很像js里面某些框架的插值写法

给label 的标签写上 {data.name} , 给image 的资源名写上 {data.image}

装备item完成,回到刚刚的PLayerScene, 把list的条目皮肤设置为刚刚创建的装备item

把list布局设置成水平布局

给scroller和list都取个id,便于后面使用

这个scroller只需要左右拖动,所以我们去 ‘所有属性’ 打开它的水平滚动,关掉垂直滚动

打开PLayerScene.ts

class PlayerScene extends EUI.Component implements  EUI.UIComponent {

public btn_return:EUI.Button;

public btn_zhuangbei:EUI.Button;

public btn_qianghua:EUI.Button;

public scr_zhuangbei:EUI.Scroller;

public list_zhuangbei:EUI.List;

public constructor() {

super();

}

protected partAdded(partName:string,instance:any):void

{

super.partAdded(partName,instance);

}

protected childrenCreated():void

{

super.childrenCreated();

// 数组数据

let dataArr:any[] = [

{image:"resource/art/profile/skillIcon01.png",name:"旋龙幻杀"},

{image:"resource/art/profile/skillIcon02.png",name:"魔魂天咒"},

{image:"resource/art/profile/skillIcon03.png",name:"天魔舞"},

{image:"resource/art/profile/skillIcon04.png",name:"痴情咒"},

{image:"resource/art/profile/skillIcon05.png",name:"无间寂"},

{image:"resource/art/profile/skillIcon06.png",name:"霸天戮杀"},

{image:"resource/art/profile/skillIcon07.png",name:"灭魂狂飙"}

]

// 把数组数据转成EUI数组

let EUIArr:EUI.ArrayCollection = new EUI.ArrayCollection(dataArr)

// 把EUI数组作为list的数据源

this.list_zhuangbei.dataProvider = EUIArr

// 隐藏进度条

this.scr_zhuangbei.horizontalScrollBar.autoVisibility = false

}

}

到这里,  玩家场景也创建完成啦~

场景管理类

我们有了两个场景,可以来做场景之间的切换了

还记得之前说的思路吗,舞台上先有一个主场景,然后点击不同按钮的时候把对应的场景添加到主场景上

这里有个需要注意的地方,子场景添加进来默认层级是高于主场景的,所以会把主场景给挡住了,而我们需要点击主场景的按钮。所以我们需要把主场景中放置按钮的Group的层级提高。

我用一个场景管理类来管理这些场景

新建一个SceneManager.ts,采用的是单例模式,要使用这个类的时候不要new SceneManager实例,而是用 SceneManager.instance 来获取到这个类的实例

这样可以保证场景管理类有且只有一个实例,便于管理操作

(使用 static修饰的方法都是静态方法,简单的说就是调用的时候不是通过实例调用,而是直接用类名来调用, 类名.方法名)

下面是一个基础的场景管理类,现在来逐步完善它的功能

/**

* 场景管理类

*/

class SceneManager {

   private _stage:egret.DisplayObjectContainer // 设置所有场景所在的舞台(根)

   private mainScene:MainScene //主场景

   private playerScene:PlayerScene //玩家场景

   // 在构造函数中创建好场景,保存到属性

   constructor() {

       this.mainScene = new MainScene()

       this.playerScene = new PlayerScene()

  }

   /**

    * 获取实例

    */

   static sceneManager:SceneManager

   static get instance(){

       if(!this.sceneManager) {

           this.sceneManager =  new SceneManager()

      } 

       return this.sceneManager

  }

   /**

    * 设置根场景

    */

   public setStage(s:egret.DisplayObjectContainer) {

       this._stage = s

  }

   // 这里补充代码……
}

首先需要管理的场景是主场景,SceneManager.instance.mainScene是获取到主场景的实例

SceneManager.instance 获取到场景管理类的实例

然后再.mainScene 获取到构造方法constructor中的 this.mainScene = new MainScene() 得到的主场景 的实例

 /**

    * 主场景

    */

   static toMainScene() {

       let stage:egret.DisplayObjectContainer = this.instance._stage // (根) 舞台

       let mainScene = SceneManager.instance.mainScene // 主场景

       // 判断主场景是否有父级(如果有,说明已经被添加到了场景中)

       if(!mainScene.parent){

           // 未被添加到场景

           // 把主场景添加到之前设置好的根舞台中

           stage.addChild(mainScene)

      } 

  }

现在到main.ts中使用场景管理类来加载主场景

当在主场景点击玩家按钮的时候,调用这个方法,切换到玩家场景

这里需要稍微改动一下之前的点击函数

打开MainScene.ts

     /**

* 切换按钮

* @param btn 参数是EUI.ToggleButton的时候切换按钮, 参数是0的时候设置为全部不选中

*/

public toggleBtn(btn:EUI.ToggleButton | number) {

// 先把所有的按钮都设置为不选中

for (let i = 0; i < this.Group_mbtn.numChildren; i++) {

let theBtn = <EUI.ToggleButton>this.Group_mbtn.getChildAt(i)

theBtn.selected = false

}

if(btn===0){

return

}

// 把传进来的btn设置为选中状态

btn = <EUI.ToggleButton>btn

btn.selected = true

// 获取当前点击的按钮的下标, 用来实现不同按钮对应的功能

// 0 1 2 3 对应 玩家, 英雄, 物品, 关于

let index = this.Group_mbtn.getChildIndex(<EUI.ToggleButton>btn)

switch (index) {

case 0:

// 调用静态方法切换到玩家场景

SceneManager.toPlayerScene()

// 把按钮的层级提高

// this.numChildren表示所有的子元素数量

this.setChildIndex(this.Group_mbtn, this.numChildren)

break

default:

break

}

}

点击玩家按钮就可以正常切换到玩家场景了,现在来实现其中的返回按钮

点击返回按钮,回到主场景,并且下面的按钮全都变成未选中状态

打开PlayerScene.ts

// 给返回按钮添加事件

this.btn_return.addEventListener(egret.TouchEvent.TOUCH_TAP, this.returnMain, this)

/**

* 回到主场景

*/

private returnMain() {

SceneManager.toMainScene()

}

点击按钮跳转回到主场景,其实就是删除掉当前覆盖在主场景上的玩家场景,主场景就能显示出来了

so,打开SceneManager.ts,完善一下刚刚的函数

/**

    * 主场景

    */

   static toMainScene() {

       let stage:egret.DisplayObjectContainer = this.instance._stage // (根) 舞台

       let mainScene = SceneManager.instance.mainScene // 主场景

       // 取消所有按钮的选中状态

       mainScene.toggleBtn(0)

       // 判断主场景是否有父级(如果有,说明已经被添加到了场景中)

       if(!mainScene.parent){

           // 未被添加到场景

           // 把主场景添加到之前设置好的根舞台中

           stage.addChild(mainScene)

      } 

       // 判断玩家场景是否有父级(是否在场景中)

       if(SceneManager.instance.playerScene.parent) {

           // 如果有就删除玩家场景

           mainScene.removeChild(SceneManager.instance.playerScene)

      }

  }

保存文件

去浏览器里就能看到效果了~

英雄场景

开始制作英雄场景 HeroScene.exml

到源码部分,修改一下两个按钮的效果

在中间放置一个Scroller,然后里面放一个List,

跟之前玩家场景其实差不多啦,现在去创建 heroListItem.exml

这里需要注意,有个checkBox,用来选中某个list

把里面的数据插值写好 {data.image} {data.name} {data.value}

checkBox是个例外,它的值不能用{data.xx}的方式来指定,我们需要创建一个单独的类

Herolist_item.ts

// 必须要继承自 EUI.ItemRenderer

class HeroList_item extends EUI.ItemRenderer{

// 选择框

public ce_select:EUI.CheckBox;

public constructor() {

super()

// 把这个 类和皮肤 联系起来

this.skinName = ‘resource/skins/skins_item/heroListItem.exml‘

}

       // 当数据改变时,更新视图

protected dataChanged() {

// isSeleted 是我们提供数据的某个字段

this.ce_select.selected = this.data.isSelected

}

}

回到HeroScene.exml, 把list的条目皮肤设置为heroListItem,并给他们设置好id

在所有属性里面把水平滚动关掉,垂直滚动打开

打开英雄场景HeroScene.ts

class HeroScene extends EUI.Component implements  EUI.UIComponent {

public btn_return:EUI.Button;

public btn_select:EUI.Button;

public scr_hero:EUI.Scroller;

public list_hero:EUI.List;

public constructor() {

super();

}

protected partAdded(partName:string,instance:any):void

{

super.partAdded(partName,instance);

}

protected childrenCreated():void

{

super.childrenCreated();

// 原始数组

let dataArr:any[] = [

{image: ‘resource/art/heros_goods/heros01.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false},

{image: ‘resource/art/heros_goods/heros02.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false},

{image: ‘resource/art/heros_goods/heros03.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: true},

{image: ‘resource/art/heros_goods/heros04.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false},

{image: ‘resource/art/heros_goods/heros05.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false},

{image: ‘resource/art/heros_goods/heros06.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false},

{image: ‘resource/art/heros_goods/heros07.png‘, name: ‘亚特伍德‘, value: ‘评价: 很特么厉害, 为所欲为‘, isSelected: false}

]

// 转成EUI数组

let EUIArr:EUI.ArrayCollection = new EUI.ArrayCollection(dataArr)

// 把list_hero数据源设置成EUIArr

this.list_hero.dataProvider = EUIArr

// 设置list_hero的项呈视器 (这里直接写类名,而不是写实例)

this.list_hero.itemRenderer = HeroList_item

}

}

现在运行一下,能看到列表已经能正确的加载,而且数组中isSeleted字段为true的第三项也被默认选中了。

现在来实现手动更改列表的选中状态

Herolist_item.ts

// 必须要继承自 EUI.ItemRenderer

class HeroList_item extends EUI.ItemRenderer{

// 选择框

public ce_select:EUI.CheckBox;

public constructor() {

super()

// 把这个 类和皮肤 联系起来

this.skinName = ‘resource/skins/skins_item/heroListItem.exml‘

// 当组件创建完成的时候触发

this.addEventListener(EUI.UIEvent.CREATION_COMPLETE, this.onComplete, this)

}

private onComplete() {

// 当select的选中状态发生改变的时候触发

this.ce_select.addEventListener(EUI.UIEvent.CHANGE, (e) => {

// this.data 就是绑定的数据, 

this.data.isSelected = this.ce_select.selected

// 把数据打印出来看看

console.log(this.data);

}, this)

}

// 当数据改变时,更新视图

protected dataChanged() {

// isSeleted 是我们提供数据的某个字段

this.ce_select.selected = this.data.isSelected

}

}

这里可能稍微有点儿绕,需要花一点时间好好理解一下代码执行的流程。

现在点击checked框的时候就能正确的修改数据的isSelected了

继续完成返回和确定选择的按钮

返回按钮其实就是把场景切换到主场景去,在场景控制类中写个方法就好

确定选择的按钮其实也是要切换回到主场景,再获取一下数据里面isSelected为true的项,并把它们显示到屏幕上

设置返回按钮,直接在点击事件中调用场景管理的方法就好了,把按钮的选择状态都清除

HeroScene.ts

// 点击返回按钮,回到主场景

this.btn_return.addEventListener(egret.TouchEvent.TOUCH_TAP, (e)=>{

SceneManager.toMainScene()

SceneManager.instance.mainScene.toggleBtn(0)

}, this)

选择功能,点击按钮的时候获取到数据源中的isSelected为true的项都保存到数组中,然后把这个数组作为参数传到场景管理里面。拿到数组就创建对应的消息显示出来就好了

HeroScene.ts

// 点击确定按钮,回到主场景,显示出选择的项

this.btn_select.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onClickSelect, this)

        /**

* 点击确定按钮

*/

onClickSelect(e) {

SceneManager.toMainScene()

SceneManager.instance.mainScene.toggleBtn(0)

// 拿到数据源

let dataProvider = this.list_hero.dataProvider

let arr:string[] = []

// 遍历数据源中所有项

for(let i = 0; i < dataProvider.length; i ++) {

let item = dataProvider.getItemAt(i)

if (item.isSelected) {

arr.push(item.name)

}

}

SceneManager.showInfo(arr)

}

SceneManager.ts

   /**

    * 在主场景显示选择的数据

    */

   static showInfo(arr:string[]) {

       let text:string = ‘你选择了: ‘

       if (arr.length === 0) {

           text = ‘厉害了什么都不选‘

      } else {

           text += arr.toString()

      }

       // 新建一个消息背景图

       let img:egret.Bitmap = new egret.Bitmap()

       img.texture = RES.getRes(‘toast-bg_png‘)

       SceneManager.instance.mainScene.addChild(img)

       img.x = SceneManager.instance.mainScene.width / 2 - img.width / 2

       img.y = 500

       img.height = 40

       // 新建一个label用来显示

       let label:egret.TextField = new egret.TextField(); 

       label.text = text

       label.size = 20

       SceneManager.instance.mainScene.addChild(label)

       label.x = SceneManager.instance.mainScene.width / 2 - label.width / 2

       label.y = 510

       label.height = 40

       // 创建一个定时器,1000毫秒后删除label

       let timer:egret.Timer = new egret.Timer(1000, 1)

       timer.start()

       timer.addEventListener(egret.TimerEvent.TIMER_COMPLETE, (e)=>{

           SceneManager.instance.mainScene.removeChild(label)

           SceneManager.instance.mainScene.removeChild(img)

      }, this)

  }

到这里,我们的游戏卡牌项目的重点已经基本完成了。

后面还有两个场景,物品场景,关于场景

物品场景其实就是EUI搭建好场景后,加上一个Scroller,然后里面放上数据列表就好,跟前面的操作都一样

关于场景根本就是添加一个图片到场景就ok

最后

如果你按照之前的几篇文章一步一步的动手实现到了这里,那么你已经完成了

1. EUI项目的创建

2. exml皮肤界面的拖拽搭建

3. EUI基本控件的使用方法: image button scorller list ……

4. loading的实现

5. 游戏场景切换:实现了场景管理类

6. 游戏开发的一些思路,代码组织方式 (虽然我的不一定是对的,但是能参考一下)

你已经很强了,所以剩下的两个场景实现起来肯定毫无问题。

我的代码还有很多可以优化的地方, 比如场景管理里面每次都要判断删除其他场景,  完全可以封装一个方法来使用。

做完这个项目最好再好好看一遍自己写的代码,重点是要明确思路,理清代码逻辑。

源码

https://github.com/hedongshu/egret-EUI-DEMO

 

原文地址:https://www.cnblogs.com/egret-edcation/p/9396979.html

时间: 2024-10-14 04:00:42

菜鸟教程 | egret EUI卡牌游戏制作的相关文章

TCG卡牌游戏研究:《炉石战记:魔兽英雄传》所做的改变

转自:http://www.gameres.com/665306.html TCG演进史 说到卡牌游戏,大家会联想到什么呢? 是历史悠久的扑克牌.风靡全球的<MTG 魔法风云会>与<游戏王>.结合数位与现实的<三国志大战>.或是在手机上掀起收集热潮的<龙族拼图>和<百万亚瑟王>? 卡牌游戏这个统称,其内容可以跟各式各样的玩法结合,而暴风雪新推出的<炉石战记>(以下简称炉石)所选择的玩法,是让玩家自行组牌.进行对战的「集换式卡牌游戏」(

卡牌游戏

卡牌游戏 个人信息:就读于燕大本科软件工程专业 目前大三; 本人博客:google搜索"cqs_2012"即可; 个人爱好:酷爱数据结构和算法,希望将来从事算法工作为人民作出自己的贡献; 编程语言:C++ 和 java ; 编程坏境:Windows 7 专业版 x64; 编程工具:vs2008; 制图工具:office 2010 powerpoint; 硬件信息:7G-3 笔记本; 真言 敢于承认不足,敢于去接触和学习,同时又沉稳而镇静 题目 百练 1003 How far can y

计蒜客 16877 卡牌游戏

题目链接:https://nanti.jisuanke.com/t/16877 题目大意:桌子上有N堆牌,每堆牌有Si张,每张牌上有个数.小明和小红玩游戏,小红女士优先,每次从任意一个牌堆顶部取出一张,小明长得丑,每次从任意一个牌堆底部取一张.假设他俩都按照最优的方式取牌,那么谁最后得到的所有牌的数字的和大. 解题思路:首先我们考虑牌个数为偶数的几堆,那么你会发现,无论小红选哪儿个,小明只要跟着她选对应牌堆底部的,那么他俩总会各选顶部或底部一半牌,而且无论谁先手都是一样,显然这样也是最优的(我不

【BZOJ3191】【JLOI2013】卡牌游戏 [DP]

卡牌游戏 Time Limit: 10 Sec  Memory Limit: 128 MB[Submit][Status][Discuss] Description N个人坐成一圈玩游戏.一开始我们把所有玩家按顺时针从1到N编号.首先第一回合是玩家1作为庄家.每个回合庄家都会随机(即按相等的概率)从卡牌堆里选择一张卡片,假设卡片上的数字为X,则庄家首先把卡片上的数字向所有玩家展示,然后按顺时针从庄家位置数第X个人将被处决即退出游戏.然后卡片将会被放回卡牌堆里并重新洗牌.被处决的人按顺时针的下一个

[JLOI2013]卡牌游戏 概率DP

[JLOI2013]卡牌游戏 概率DP 题面 \(dfs\)复杂度爆炸,考虑DP.发现决策时,我们只用关心当前玩家是从庄家数第几个玩家与当前抽到的牌是啥.于是设计状态\(f[i][j]\)表示有\(i\)个人时,从庄家数第\(j\)个人的胜率.又因为此时终态确定\(f[1][1]=1\)(只有一个人时那个人胜率为100%),所以倒推回去. 转移时,枚举抽到的牌,算出从庄家数第\(t\)个会出局,那么下一局庄家就是第\(t+1\)个,当前局第\(j\)个就是下一局的第\(j-t(t< j)\)或\

JAVA面向对象编程课程设计——UNO卡牌游戏

一.团队介绍 团队名称:吉祥三宝 成员名称 任务分配 个人博客连接 赖慧颖(组长) 嘤 UNO卡牌游戏-个人博客 黄雅静 嘤 UNO卡牌游戏-个人博客 杨鸿漾 嘤 UNO卡牌游戏-个人博客 二.项目Git地址 UNO卡牌游戏 三.项目git提交记录截图 四.前期调查 五.项目功能架构图.主要功能流程图 六.面向对象设计包图.类图 七.项目运行截图或屏幕录制 八.项目关键代码 九.项目代码扫描结果及改正 十.项目总结 原文地址:https://www.cnblogs.com/yhy949/p/12

[JLOI2013]卡牌游戏

题目描述 N个人坐成一圈玩游戏.一开始我们把所有玩家按顺时针从1到N编号.首先第一回合是玩家1作为庄家.每个回合庄家都会随机(即按相等的概率)从卡牌堆里选择一张卡片,假设卡片上的数字为X,则庄家首先把卡片上的数字向所有玩家展示,然后按顺时针从庄家位置数第X个人将被处决即退出游戏.然后卡片将会被放回卡牌堆里并重新洗牌.被处决的人按顺时针的下一个人将会作为下一轮的庄家.那么经过N-1轮后最后只会剩下一个人,即为本次游戏的胜者.现在你预先知道了总共有M张卡片,也知道每张卡片上的数字.现在你需要确定每个

JLOI 2013 卡牌游戏

问题描述: N个人坐成一圈玩游戏.一开始我们把所有玩家按顺时针从1到N编号.首先第一回合是玩家1作为庄家.每个回合庄家都会随机(即按相等的概率)从卡牌堆里选择一张卡片,假设卡片上的数字为X,则庄家首先把卡片上的数字向所有玩家展示,然后按顺时针从庄家位置数第X个人将被处决即退出游戏.然后卡片将会被放回卡牌堆里并重新洗牌.被处决的人按顺时针的下一个人将会作为下一轮的庄家.那么经过N-1轮后最后只会剩下一个人,即为本次游戏的胜者.现在你预先知道了总共有M张卡片,也知道每张卡片上的数字.现在你需要确定每

bzoj 3191: [JLOI2013]卡牌游戏

Description N个人坐成一圈玩游戏.一开始我们把所有玩家按顺时针从1到N编号.首先第一回合是玩家1作为庄家.每个回合庄家都会随机(即按相等的概率)从卡牌堆里选择一张卡片,假设卡片上的数字为X,则庄家首先把卡片上的数字向所有玩家展示,然后按顺时针从庄家位置数第X个人将被处决即退出游戏.然后卡片将会被放回卡牌堆里并重新洗牌.被处决的人按顺时针的下一个人将会作为下一轮的庄家.那么经过N-1轮后最后只会剩下一个人,即为本次游戏的胜者.现在你预先知道了总共有M张卡片,也知道每张卡片上的数字.现在