Spine.js 2D游戏动画

Spine.js 2D游戏动画

2020-05-18 42

最近迷上了《碧蓝航线》手游,里面的SD小人超呆萌可爱,想研究下是用dragonbones还是spine来做的

在各种找教程无意间发现贴吧大佬拆包的资源,发现.skel文件里面用的是spine v3.6.52版本,于是去https://github.com/EsotericSoftware/spine-runtimes/找了3.6版本照着官方webgl案例,真的可以加载模型

不过不想自个拆包,毕竟小黄鸡更新那么频繁,皮肤有多,维护起来麻烦,所以又去github找到大佬的拆包资源

于是参考了:https://github.com/Pelom777/AzurLaneSD

改成了vue版本的,并把角色名称拼音换成中文,瓜游的模型文件命名用拼音,捣鼓了好久才用 node-pinyin 把拼音改成了中文

Demo:https://timelessq.com/toolkit/spine

仅用于学习,勿作商业用途,侵权请联系删除

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Spine</title>
  <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
  <link rel="stylesheet" href="index.css">
</head>
<body>
  <div id="app" element-loading-text="拼命加载中" element-loading-background="rgba(0, 0, 0, 0.1)">
    <div class="spine-tool">
      <el-select v-model="seleteSkeleton" filterable :loading="listLoading" size="medium" :filter-method="filterSkel" @change="onChangeSkel">
        <el-option
          v-for="(item, index) in filterOptions"
          :key="index"
          :label="item.name"
          :value="item.value"
        >
          <span style="float: left">{{ item.jp ? `${item.name}(${item.jp})` : item.name }}</span>
          <span style="float: right; color: #606266; font-size: 12px">{{ item.remark }}</span>
        </el-option>
      </el-select>
      <el-select v-model="selectAnimation" size="medium" @change="onChangeAnimation">
        <el-option
          v-for="item in animationOptions"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
    </div>
    <canvas id="canvas" class="spine-canvas"></canvas>
    <div v-loading="skelLoading" element-loading-text="拼命加载中" element-loading-background="rgba(0, 0, 0, 0)" class="spine-loading" />
    <div class="spine-scale">
      <el-slider v-model="spineScale" :step="0.01" :min="0.5" :max="1.5" @input="onScaleChange" />
    </div>
  </div>
  <script src="https://unpkg.com/vue/dist/vue.js"></script>
  <script src="https://unpkg.com/element-ui/lib/index.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios@0.21.1/dist/axios.min.js"></script>
  <script src="https://pelom.gitee.io/azurlanesd/src/build/spine-webgl.js"></script>
  <script src="index.js"></script>
</body>
</html>

index.css

body{
  margin: 0;
}
.spine{
  position: fixed;
  width: 100%;
  height: 100%;
}
.spine-tool{
  position: fixed;
  top: 30px;
  left: 0;
  width: 100%;
  text-align: center;
}
.spine-tool .el-select{
  margin-bottom: 10px;
}
.spine-scale{
  position: fixed;
  bottom: 30px;
  left: 50%;
  width: 410px;
  margin-left: -205px;
}
.spine-loading {
  position: fixed;
  left: 50%;
  bottom: 90px;
  width: 100px;
  height: 60px;
  margin-left: -50px;
}
@media (max-width: 576px) {
  .spine-tool {
    top: 15px;
  }
  .spine-scale{
    left: 0;
    bottom: 15px;
    width: 100%;
    padding: 0 15px;
    margin-left: 0;
  }
  .spine-loading{
    bottom: 75px;
  }
}

index.js

var app = new Vue({
  el: '#app',
  data() {
    return {
      seleteSkeleton: 'lafei_4',
      skelOptions: [],
      listLoading: false,
      filterOptions: [],
      selectAnimation: '',
      animationOptions: [],
      checkboxGroup1: '',
      spineScale: 1,
      skelLoading: false
    }
  },
  mounted() {
    this.initSpine()
    this.fetchList()
  },
  methods: {
    async fetchList() {
      this.listLoading = true
      await axios.get('//api.timelessq.com/spine/lists').then(res => {
        this.filterOptions = this.skelOptions = res.data.data
      }).catch(() => {})
      this.listLoading = false
    },
    fetchAssets() {
      return axios.get('//api.timelessq.com/spine', {
        params: {
          id: this.seleteSkeleton,
          isuseCDN: true
        }
      }).then(res => {
        this.assetsData = res.data.data
        this.loadAsset()
      })
    },
    async initSpine() {
      // Setup canvas and WebGL context. We pass alpha: false to canvas.getContext() so we don't use premultiplied alpha when
      // loading textures. That is handled separately by PolygonBatcher.
      this.spineCanvas = document.getElementById('canvas')
      console.log(window.innerWidth)
      this.spineCanvas.width = window.innerWidth
      this.spineCanvas.height = window.innerHeight
      const config = { alpha: false }
      this.gl = this.spineCanvas.getContext('webgl', config) || this.spineCanvas.getContext('experimental-webgl', config)
      if (!this.gl) {
        alert('WebGL is unavailable.')
        return
      }

      // Create a simple shader, mesh, model-view-projection matrix, SkeletonRenderer, and AssetManager.
      this.shader = spine.webgl.Shader.newTwoColoredTextured(this.gl)
      this.batcher = new spine.webgl.PolygonBatcher(this.gl)
      this.mvp = new spine.webgl.Matrix4()
      this.mvp.ortho2d(0, 0, this.spineCanvas.width - 1, this.spineCanvas.height - 1)
      this.skeletonRenderer = new spine.webgl.SkeletonRenderer(this.gl)
      this.assetManager = new spine.webgl.AssetManager(this.gl)
      await this.fetchAssets()
      requestAnimationFrame(this.load)
    },
    loadAsset() {
      const { atlas, skelBinary, skelJson } = this.assetsData
      if (skelJson) {
        this.assetManager.loadText(skelJson)
      } else {
        this.assetManager.loadBinary(skelBinary)
      }
      this.assetManager.loadTextureAtlas(atlas)
    },
    load() {
      this.skelLoading = true
      // Wait until the AssetManager has loaded all resources, then load the skeletons.
      if (this.assetManager.isLoadingComplete()) {
        this.selectAnimation = 'normal'
        this.activeSkeleton = this.loadSkeleton(false)
        this.isChange = false
        this.lastFrameTime = Date.now() / 1000
        this.setupAnimation()
        requestAnimationFrame(this.render)
      } else {
        requestAnimationFrame(this.load)
      }
    },
    loadSkeleton(premultipliedAlpha, skin) {
      const { atlas, skelBinary, skelJson } = this.assetsData
      if (skin === undefined) skin = 'default'
      // Load the texture atlas using name.atlas from the AssetManager.
      const atlasData = this.assetManager.get(atlas)
      // Create a AtlasAttachmentLoader that resolves region, mesh, boundingbox and path attachments
      const atlasLoader = new spine.AtlasAttachmentLoader(atlasData)
      // Set the scale to apply during parsing, parse the file, and create a new skeleton.
      let skeletonData
      if (skelJson) {
        var skeletonJson = new spine.SkeletonJson(atlasLoader)
        skeletonData = skeletonJson.readSkeletonData(this.assetManager.get(skelJson))
      } else {
        // Create a SkeletonBinary instance for parsing the .skel file.
        // var skeletonBinary = new spine.SkeletonBinary(atlasLoader);
        var skeletonBinary = new spine.SkeletonBinary(atlasLoader)
        skeletonData = skeletonBinary.readSkeletonData(this.assetManager.get(skelBinary))
      }
      const skeleton = new spine.Skeleton(skeletonData)
      this.root = skeletonData.findBone('root')
      skeleton.setSkinByName(skin)
      const bounds = this.calculateBounds(skeleton)
      // // Create an AnimationState, and set the initial animation in looping mode.
      const animationStateData = new spine.AnimationStateData(skeleton.data)
      const animationState = new spine.AnimationState(animationStateData)
      if (skeleton.data.findAnimation(this.selectAnimation) == null) {
        this.selectAnimation = skeleton.data.animations[0].name
      }
      animationState.setAnimation(0, this.selectAnimation, true)
      // Pack everything up and return to caller.
      return { skeleton, state: animationState, bounds, premultipliedAlpha }
    },
    calculateBounds(skeleton) {
      skeleton.setToSetupPose()
      skeleton.updateWorldTransform()
      const offset = new spine.Vector2()
      const size = new spine.Vector2()
      skeleton.getBounds(offset, size, [])
      return { offset, size }
    },
    render() {
      this.skelLoading = false
      if (this.isChange) {
        this.load()
        return
      }
      const now = Date.now() / 1000
      const delta = now - this.lastFrameTime
      this.lastFrameTime = now
      // Update the MVP matrix to adjust for canvas size changes
      this.resize()
      this.gl.clearColor(0.5, 0.5, 0.5, 1)
      this.gl.clear(this.gl.COLOR_BUFFER_BIT)
      // Apply the animation state based on the delta time.
      const state = this.activeSkeleton.state
      const skeleton = this.activeSkeleton.skeleton
      const premultipliedAlpha = this.activeSkeleton.premultipliedAlpha
      state.update(delta)
      state.apply(skeleton)
      skeleton.updateWorldTransform()
      // Bind the shader and set the texture and model-view-projection matrix.
      this.shader.bind()
      this.shader.setUniformi(spine.webgl.Shader.SAMPLER, 0)
      this.shader.setUniform4x4f(spine.webgl.Shader.MVP_MATRIX, this.mvp.values)
      // Start the batch and tell the SkeletonRenderer to render the active skeleton.
      this.batcher.begin(this.shader)
      this.skeletonRenderer.premultipliedAlpha = premultipliedAlpha
      this.skeletonRenderer.draw(this.batcher, skeleton)
      this.batcher.end()
      this.shader.unbind()
      requestAnimationFrame(this.render)
    },
    resize() {
      const w = this.spineCanvas.clientWidth
      const h = this.spineCanvas.clientHeight
      const bounds = this.activeSkeleton.bounds
      if (this.spineCanvas.width !== w || this.spineCanvas.height !== h) {
        this.spineCanvas.width = w
        this.spineCanvas.height = h
      }
      // magic
      const centerX = bounds.offset.x + bounds.size.x / 2
      const centerY = bounds.offset.y + bounds.size.y / 2
      const scaleX = bounds.size.x / this.spineCanvas.width
      const scaleY = bounds.size.y / this.spineCanvas.height
      let scale = Math.max(scaleX, scaleY) * 1.2
      if (scale < 1) scale = 1
      const width = this.spineCanvas.width * scale
      const height = this.spineCanvas.height * scale
      this.mvp.ortho2d(centerX - width / 2, centerY - height / 2, width, height)
      this.gl.viewport(0, 0, this.spineCanvas.width, this.spineCanvas.height)
    },
    setupAnimation() {
      const skeleton = this.activeSkeleton.skeleton
      this.animationOptions = skeleton.data.animations.map(item => {
        return {
          label: item.name,
          value: item.name
        }
      })
    },
    async onChangeSkel() {
      await this.fetchAssets()
      this.isChange = true
    },
    onChangeAnimation(animationName) {
      const state = this.activeSkeleton.state
      const skeleton = this.activeSkeleton.skeleton
      skeleton.setToSetupPose()
      state.setAnimation(0, animationName, true)
    },
    onScaleChange(val) {
      if (!this.root) return
      this.root.scaleX = this.root.scaleY = val
    },
    filterSkel(keyword) {
      if (keyword) {
        this.filterOptions = this.skelOptions.filter(item => JSON.stringify(item).toLowerCase().includes(keyword))
      } else {
        this.filterOptions = this.skelOptions
      }
    }
  }
})

 

评论(0)

还没有评论,快来抢第一吧