实现红警式的建筑物拖拽生成特效

上一节,我们完成了建筑物选择面板的创建,本节我们基于上一节工作的基础上,实现建筑物选择后,拖拽生成效果。为了让游戏的视觉效果更加栩栩如生,当用户选择一个建筑物后,有一个半透明的建筑物图标会随着鼠标移动,当用户在画面上点击后,建筑物会在鼠标指定的位置进行建造,而且建造是是一个动态过程,玩过红警的同学想必对这种情形不会陌生。

我们本节要实现的效果如下所示,首先用户在建筑物选择面板中选取要建造的对象:

选择后,对应建筑物的半透明图标会跟随着用户鼠标在界面上移动:

如果用户鼠标挪动到的方块上面已经被其他建筑物所占据的话,半透明图标会显示出红亮色,表示当前区域不能放置建筑物:

当用户把建筑物挪动空余的方格上,并点击鼠标后,建筑物就会出现在所点击的方格上,实际上建筑物不是鼠标点击后就一下子出现在方格上的,我们后面会实现建筑物构建的一个动态过程,通过一系列的动画转变过程,显示出建筑物建造要经历的若干个阶段,有点像你玩‘帝国时代’建造一个兵营时的那种效果,本节基于篇幅所限,我们暂时实现用户点击后,建筑物就直接出现在页面上:

接下来,我们就从代码角度来探讨上面功能的实现。首先要做的,是在建筑物选择面板出现时,程序应该判断当前玩家具有的钱币和人口数量,根据这些资源情况来决定玩家可以选择哪种建筑物,如果资源不足的话,在选择面板上,对应的建筑物就不存在build按钮,这样用户就不能选择建筑对应建筑物,于是我们现在Constant.vue里添加如下代码:

<script>
  import Vue from 'vue'
  export default {
  ....
  // change here
    CoinsGenerator: {
      className: 'ConsGenerator',
      needCoins: 20,
      needPopulations: 10,
      power: 0
    },
    PowerSupply: {
      className: 'PowerSupply',
      needCoins: 10,
      needPopulations: 0,
      power: 15
    },
    Merchant: {
      className: 'Merchant',
      needCoins: 150,
      needPopulations: 20,
      power: 0
    }
   ....
}

上面代码表明,钱币厂也就是CoinsGenerator 这个建筑物需要耗费20金币和10个人口,其他建筑物的逻辑于此类似。接着来到buildingpanelcomponent.vue文件,添加如下代码:

setupBuildingButton (i) {
var b = this.buildings[i]
....
// change here
// 判断当前是否有足够的钱币,电量和人口去建造建筑物
var hasEnoughPowerSupplies = Constant[b.name].needPopulations === 0 || (this.gameSceneComponent.powerSupplies - this.gameSceneComponent.populations >= Constant[b.name].needPopulations)

        var hasEnoughCoins = (this.gameSceneComponent.coins >= Constant[b.name].needCoins)

        if (hasEnoughPowerSupplies && hasEnoughCoins) {
          button.visible = true
          buttonDisabled.visible = false
        } else {
          button.visible = false
          buttonDisabled.visible = true
        }

}

setupBuildingButton 这个函数是用来设置面板上建筑物的选择按钮的,它通过Constant组件里面我们刚添加的代码逻来判断,用户是否有足够的资源来建筑当前指定的建筑物,如果资源不足,我们就让buttonDisabled的visible属性为真,于是在面板上的建筑物图案上,中间那个’build’按钮就不会出现。如果资源足够的话,那么button对象的visible属性就是true,于是面板中建筑物图案中间的’build’按钮就会显示出来。

接下来我们看看建筑物拖拽生成的基本逻辑: 1, 用户在面板上点击要建筑物。 2, 程序把建筑物对应的图片加载到页面,并设置成半透明 3,追踪鼠标移动轨迹,让半透明图片跟随着鼠标移动 4,计算当前鼠标落入方块所在的行和列 5,获得方块的中心位置坐标,并把半透明图片的中心设置为与方块中心一致,于是半透明图片就正好落入在方块中。 6,如果当前方块已经包含其他建筑物,那么让图片显示出高亮的红色。 6,当用户点击鼠标后,去除图片的半透明效果,并把建筑物图片放置在鼠标点击时所在的方块上方。

接下来,我们按照上面几个步骤来实现代码。当页面加载时,当用户在选择面板上点击’Build’按钮时,我们需要响应点击事件,代码如下:

setupBuildingButton (i) {
  var b = this.buildings[i]
  ....
  var _this = this
        // change here
  button.on('click', function () {
  // 从这里开始触发整个建筑物拖拽效果
   console.log('building selected:' + b.name)
   //
   _this.gameSceneComponent.buildingTypeToBePlaced = b.name
   _this.gameSceneComponent.isCreatingNewBuilding = true
   _this.buildingPanel.visible = false
   _this.cancelBuildBtn.visible = true
   Constant.Event.$emit(Constant.MSG_NEWBUILDING_READY)
 })
....
}

代码中的变量button,对应的就是选择面板上,对应建筑物的’Build’按钮,其中的gameSceneComponent对应的就是gameSceneComponent组件实例,代码把用户选取的建筑物名字存储到buildingTypeToBePlaced变量中,然后发出一个MSG_NEWBUILDING_READY消息,响应这个消息的是gameSceneComponent组件。我们回到gamescenecomponent.vue文件,看看相应的消息响应代码:

mounted () {
      this.init()
      // change here
      Constant.Event.$emit(Constant.MSG_CREATE_BUILDINGS, this)

      Constant.Event.$on(Constant.MSG_NEWBUILDING_READY, function () {
        this.newBuildingToBePlaced()
      }.bind(this))
    },
    newBuildingToBePlaced () {
    this.cityLayer.removeChild(this.ghostBuilding)
    this.ghostBuilding = this.getBuildingByName(this.buildingTypeToBePlaced)
    this.ghostBuilding.alpha = 0.5
    this.ghostBuilding.visible = false
    this.cityLayer.addChild(this.ghostBuilding)
 },
 getBuildingByName (name) {
    if (name === 'PowerSupply') {
      console.log('PowerSupply')
      return this.powerSupply()
    }
    if (name === 'Merchant') {
      console.log('Merchant')
      return this.merchant()
    }
    if (name === 'CoinsGenerator') {
      console.log('CoinsGenerator')
      return this.coinsGenerator()
    }
}

一旦发现MSG_NEWBUILDING_READY消息被发送出来后,gameSceneComponent组件则调用newBuildingToBePlaced函数启动建筑物的拖拽生成流程。在第二个函数中,ghostBuilding对应的就是跟随着鼠标挪动的半透明建筑物图片对象,getBuildingByName根据建筑物的名字,把建筑物图标加载到页面中,然后把其alpha 属性设置为0.5,这样图片在页面上就会显示出半透明效果。powerSupply, mechant, coinsGenerator三个函数的作用是,将选中建筑物的图片加载到浏览器中,其代码如下:

coinsGenerator () {
  var obj = this.tile('../../static/images/coins-generator.png')
  obj.width = 86
  obj.height = 43
  obj.regX = 0
  obj.regY = 94
  return obj
},
merchant () {
  var obj = this.tile('../../static/images/merchant.png')
  obj.width = 86
  obj.height = 43
  obj.regX = 0
  obj.regY = 43
  return obj
},
powerSupply () {
  var obj = this.tile('../../static/images/power-supply.png')
  obj.width = 86
  obj.height = 43
  obj.regX = 0
  obj.regY = 51
  return obj
},

以上代码完成了步骤1,2,接着我们要让加载的半透明建筑物图标跟随着鼠标移动,因此,代码必须捕捉鼠标移动时的坐标信息:

methods: {
 init () {
 ....
 // change here
 this.cityLayer = this.cityLayer()
 this.stage.on('stagemousemove', this.handleStageMouseMove)
 this.stage.on('click', this.handleCityLayerClick)
 ....
}

一旦鼠标移动时,stage容器对象会产生stagemousemove消息,我们只要响应该消息就可以捕捉到鼠标移动时的相应坐标,如果鼠标点击事件发生的话,stage容器对象还会发生click消息,因此我们对该消息也要添加相应的响应函数。

      // change here
      // 将屏幕鼠标坐标转换成建筑物拖放位置
      screenToIsoCoord (screenX, screenY) {
        var ix = Math.floor((screenY * this.tileWidth + screenX * this.tileHeight) / (this.tileWidth * this.tileHeight))
        var iy = Math.floor((screenY * this.tileWidth - screenX * this.tileHeight) / (this.tileWidth * this.tileHeight)) + 1
        return {x: ix, y: iy}
      },
      isoToScreenCoord (isoX, isoY) {
        var sx = (isoX - isoY) * this.tileWidth / 2
        var sy = (isoX + isoY) * this.tileHeight / 2
        return new this.cjs.Point(sx, sy)
      },
      ....
handleStageMouseMove (e) {
        if (!this.isCreatingNewBuilding) {
          if (this.ghostBuilding != null) {
            this.ghostBuilding.visible = false
          }
          return
        }

        this.showGhostBuilding(e.stageX, e.stageY)
      },
showGhostBuilding (x, y) {
        this.ghostBuilding.visible = true
        // 先把相对于整个画面的坐标坐标转换为相对于城市图层的坐标
        var localPt = this.cityLayer.globalToLocal(x, y)
        // 根据坐标所在的位置计算鼠标所指向的方格在第几行第几列
        var isoCoord = this.screenToIsoCoord(localPt.x, localPt.y)
        // 根据上面得到的方格,计算其中心位置所在城市图层中的具体坐标
        var tileScreenCoord = this.isoToScreenCoord(isoCoord.x, isoCoord.y)
        // 把半透明的建筑物图片显示在鼠标所在的方块内
        this.ghostBuilding.x = tileScreenCoord.x
        this.ghostBuilding.y = tileScreenCoord.y
        this.ghostBuilding.filters = []

        // 如果方块内已经被其他建筑物占据,那么让跟随着鼠标的图片显示出红色
        var isTileAvailable = (this.cityLayer.data[isoCoord.y] && this.cityLayer.data[isoCoord.y][isoCoord.x] === 'Tile')
        if (!isTileAvailable) {
          this.ghostBuilding.filters = [new this.cjs.ColorFilter(1, 0, 1, 1)]
        }
        this.ghostBuilding.cache(0, 0, 100, 100)
      }

在handleStageMouseMove函数的实现中,他通过传入参数e获得鼠标的当前坐标,e.stageX和e.stageY,然后传给函数showGhostBuilding,由后者复杂实现步骤4,5,6. 首先我们需要根据当前鼠标坐标来确定,鼠标此时落入在哪一个方块,此时我们需要先做一个坐标系的转换:

根据上图,鼠标坐标其实是相对以最外层stage容器的,由于建筑物所在的方块是在城市图层,因此要把相对于stage容器的鼠标坐标转换成相对于城市图层的坐标位置,语句:

this.cityLayer.globalToLocal(x, y)

的作用是把鼠标坐标从stage容器转换为城市图层相对应的位置。然后计算当前鼠标所落入的方块是在第几行,第几列,然后再从Tiles二维数组中找到对应的方块对象,获得它的中心为止,并计算该位置相对于城市图层坐标轴的坐标,这些工作对应的就是下面几行代码:

 // 根据坐标所在的位置计算鼠标所指向的方格在第几行第几列
 var isoCoord = this.screenToIsoCoord(localPt.x, localPt.y)
 // 根据上面得到的方格,计算其中心位置所在城市图层中的具体坐标

screenToIsoCoord,isoToScreenCoord 这两个函数负责坐标的转换工作,他们的实现逻辑需要一些数学运算,我们不需要知道他们的具体实现,但只要了解他们的具体作用就可以了。当我们知道当前鼠标指向的方块的中心位置后,我们就可以把半透明的图片放置在方块上,代码如下:

// 把半透明的建筑物图片显示在鼠标所在的方块内
this.ghostBuilding.x = tileScreenCoord.x
this.ghostBuilding.y = tileScreenCoord.y

如果此时方块上已经有了其他建筑物,那么我们就让建筑物图标红色高亮,表明当前方块不能放置建筑物:

// 如果方块内已经被其他建筑物占据,那么让跟随着鼠标的图片显示出红色
var isTileAvailable = (this.cityLayer.data[isoCoord.y] && this.cityLayer.data[isoCoord.y][isoCoord.x] === 'Tile')
if (!isTileAvailable) {
   this.ghostBuilding.filters = [new this.cjs.ColorFilter(1, 0, 1, 1)]
}
this.ghostBuilding.cache(0, 0, 100, 100)

data是一个与城市图层中的二维网格对应的数组,如果网格没有被其他建筑物所占据,那么网格所在的行和列,对应到data这个二维数组上所得到的值就是’Tile’字符串,如果根据网格所在的行和列到data数组中查询,得到的字符串不是’Tile’时,那意味着对应网格已经被其他建筑物占据了。于是我们要实现建筑物图标的红色高亮,实现高亮效果的办法是,给建筑物图标对象添加一个颜色过滤器,也就是ColorFilter(1,0,1,1).这句代码作用是创建一个红色的颜色过滤器,如果要想把红色显示出来,就必须调用对象的cache函数,就如上面代码所做的一样。对于颜色过滤器的原理,我们无需过于纠结,只要明白上面代码中的后两行能够使得图片产生红色高亮的效果就可以了。

当选定好建筑物所在的方块后,点击鼠标,程序就会把建筑物放置到对应的方块上,相应的实现代码为:

handleCityLayerClick (e) {
        // 将鼠标相对于舞台容器的坐标转换为城市图层对应的坐标
        var localPt = this.cityLayer.globalToLocal(e.stageX, e.stageY)
        // 获得鼠标指向的方块在第几行第几列
        var isoCoord = this.screenToIsoCoord(localPt.x, localPt.y)
        // 判断当前方块是否可以放置建造物
        var isTileAvailable = (this.cityLayer.data[isoCoord.y])
        isTileAvailable = (this.cityLayer.data[isoCoord.y][isoCoord.x] === 'Tile')
        if (this.isCreatingNewBuilding && isTileAvailable) {
          console.log('put buidling')
          // 获取建筑物所需钱币数
          var needCoins = Constant[this.buildingTypeToBePlaced].needCoins
          console.log('needCoins', needCoins)
          this.coins -= needCoins
          // 通知buildingPanel,建筑物成功建造
          Constant.Event.$emit(Constant.MSG_PLACED_BUILDING)
          console.log('after send msg')
          this.isCreatingNewBuilding = false
          this.ghostBuilding.visible = false
          // 记录下当前方块存放的建筑物信息
          var newBuildingData = this.building(isoCoord.x, isoCoord.y, this.buildingTypeToBePlaced)
          this.buildingList.push(newBuildingData)
          console.log('redraw layer')
          // 重绘城市图层,把刚放下的建筑物在方格中绘制出来
          this.redraw(this.cityLayer, this.cityLayer.tiles)
        }

一旦鼠标点击时,我们先做一系列坐标转换,然后判断当前方块是否可以放置建筑物,如果可以,那么我们计算当前建筑物所需要的钱币和人口,减掉这些资源后,调用redraw函数,把建筑物绘制到相应的方块上,同时把当前放置的建筑物相关信息记录到数组buildingList中。我们再看redraw函数的相关实现:

cityLayer () {
        var obj = this.layer()
        var bg = new this.cjs.Bitmap('../../static/images/city-bg.png')
        bg.regX = 370
        bg.regY = 30
        obj.addChild(bg)
        // 9 * 9 grids
        obj.cols = obj.rows = 9
        var tiles = new this.cjs.Container()
        obj.tiles = tiles
        obj.addChild(tiles)

        obj.viewMap = this.create2DArray(obj.rows, obj.cols, 'Tile')
        // change here
        // 用数组记录当前放下的建筑物类型
        obj.data = this.create2DArray(obj.rows, obj.cols, 'Tile')
        obj.x = this.gameWidth / 2 - this.tileWidth / 2
        obj.y = this.gameHeight / 2 - (obj.rows - 1) * this.tileHeight / 2
        this.redraw(obj, tiles)
        return obj
      },
      redraw (layer, tiles) {
        // change here
        // 先创建一个二维数组,然后从建筑物记录数组中找到当前已经放置到方格里的建筑物,根据建筑物所在的方格坐标填充到数组中
        var newDataMap = this.create2DArray(layer.rows, layer.cols, 'Tile')
        for (var i = 0, len = this.buildingList.length; i < len; i++) {
          var b = this.buildingList[i]
          var className = b.name
          newDataMap[b.y][b.x] = className
        }

        for (i = 0; i < layer.rows; i++) {
          for (var j = 0; j < layer.cols; j++) {
            var t = this.tile()
            if (layer.data[i][j] !== newDataMap[i][j]) {
              tiles.removeChild(layer.viewMap[i][j])
              t = this.getBuildingByName(newDataMap[i][j])
            }
            t.x = (j - i) * (this.tileWidth / 2)
            t.y = (j + i) * (this.tileHeight / 2)
            tiles.addChild(t)
            layer.viewMap[i][j] = t
          }
        }

        layer.data = newDataMap
      },

在redraw函数中,它先创建一个对应于城市图层中方块的二维数组,接着从buildingList中获得当前已经放置到方块中的建筑物信息,获得这些建筑物所在方块的行和列,然后在对应的二维数组中,根据给定的行和列,把建筑物的名字设置到二维数组对应元素上。

然后再次遍历方块所对应的二维数组data,如果发现对应的行和列所属元素不是字符串’Tile’时,我们就把对应建筑物的图标加载到页面,并把该图标绘制到对应方块所在的位置上,要不然就仍然加载方块图案,并绘制到相应位置上。添加完上面的代码后,我们就可以实现本文开头所描述的效果了。

原文发布于微信公众号 - Coding迪斯尼(gh_c9f933e7765d)

原文发表时间:2017-11-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IMWeb前端团队

网格系统 CSS Grid Layout

听闻w3cplus大漠在第三届CSS Conf上的演讲主题是CSS Grid Layout,吓得我赶紧抛下红尘俗事闭门谢客苦心钻研,唯恐脚步太慢,遥望大漠一骑绝...

2188
来自专栏练小习的专栏

带你轻松打开svg滤镜的大门

上次和大家一起,用最简单直白,轻松粗暴的方式学习了一遍SVG动画,这次我们再一起来搞点不一样的东西,SVG滤镜的实现。 SVG滤镜绝对称得上是他最强大的功能之一...

2198
来自专栏知道一点点

CSS3 基础知识[转载minsong的博客]

CSS3 基础知识 1.边框     1.1 圆角  border-radius:5px 0 0 5px;     1.2 阴影  box-shadow:2px...

2536
来自专栏黑泽君的专栏

4道html笔试小题

1625
来自专栏我就是马云飞

自定义角标库

前言 角标的需求在app是经常需要用到的,比如未读通知/信息等,一般,我们可以通过嵌套相对布局的方式来设置角标,但是除了TextView,可能Butt...

2897
来自专栏封碎

Android画图之Matrix(一) 博客分类: Android AndroidBlog

Matrix ,中文里叫矩阵,高等数学里有介绍,在图像处理方面,主要是用于平面的缩放、平移、旋转等操作。

952
来自专栏柠檬先生

SVG 使用

SVG即Scalable Vector Graphics可缩放矢量图形,使用XML格式定义图形, 主要优势在于可缩放的同时不会影响图片的质量。 SVG 在htm...

2289
来自专栏阮一峰的网络日志

Flex 布局教程:实例篇

上一篇文章介绍了Flex布局的语法,今天介绍常见布局的Flex写法。 你会看到,不管是什么布局,Flex往往都可以几行命令搞定。 ? 我只列出代码,详细的语法解...

43412
来自专栏天天

框架设计续集(三)

1053
来自专栏HTML5学堂

CSS3蒙版 — 元旦快乐!

相信大家如果对PS有所了解都知道里面有蒙版遮罩层的效果,可我们在这里并不打算介绍PS的蒙版效果,而是介绍在内核为-webkit的浏览器中通过CSS3的新属性-w...

39310

扫码关注云+社区

领取腾讯云代金券