登录内测(欢迎小伙伴们通过右侧登录按钮进行注册和登录)
终结Phaser横屏适配
作者:channingbreeze
日期:2017-07-05
横版游戏一直占据着游戏界的半壁江山,但是对于H5游戏来说,它本身没有权利要求浏览器横屏,这给横版游戏很大的限制。本文探讨在Phaser中,如何完美地解决这个问题,方案的灵感来自QQ群友,在此表示感谢。

Phaser的屏幕适配问题是群里讨论得最多的问题,也是很关键的一个问题。

基本看过我的视频的同学,或者稍微用过Phaser的同学都应该知道了,Phaser中的ScaleManager是专门用来解决Phaser屏幕适配问题的。如果不知道ScaleManager是什么的同学,建议去看看我的视频。

在了解了ScaleManager之后,我们还会有两个问题。第一、ScaleManager有没有办法做到不变形,有能充满整个屏幕?第二、怎么让浏览器横屏?

首先,我们来说第一个问题,这其实是一个数学问题。这个问题的本质是,两个宽高比不想等的矩形,有没有办法通过等比缩放让它们完全重叠?相信任何一个小学数学学得还不错的人都能给出答案,不可能。

但是有没有办法解决这个问题呢?当然有,那就是动态设置游戏区域,拿到屏幕大小之后再等比缩放成一个游戏区域大小不就行了。但是这会带来一个问题,那就是你的元素无法按照坐标来定位了,因为你不知道游戏的宽高是多少,它和屏幕尺寸有关。这在大多数情况下会让你很难受,但是在一种情况下不会,那就是你的世界大小比你的游戏区域大,这样,你可以固定世界大小,然后元素按照世界大小来定位。

当然,我们还有别的方法,那就是在html中做一些背景,可以让游戏融入到背景中,而不是出现两边的黑边或者白边。具体使用什么方法,就看大家自己的实际情况了。

接下来说说本文重头戏:横屏适配。

很多人问我,html能不能让浏览器强制横屏,phaser中设置了横屏为什么没有用?答案肯定是不能,也没有用。其实道理很简单,因为浏览器是html的环境,html是没有权利去改变它外在的环境的,至少现在不能。

所以我们会看到很多游戏,打开之后,它建议你横屏玩耍。但是这样其实代价很大,因为很多人并不知道怎么横屏。其实我想要的是这样的效果:

也就是虽然是竖屏,但是游戏以横屏的样子展示,这样,再傻的用户都知道要把手机横过来玩吧?那么当用户横过手机的时候,有两种情况,第一,用户的手机设置了竖屏锁定,即使横过来,其实还是竖屏,这种情况没问题。第二,用户没有设置竖屏锁定,横过来之后,系统切换成横屏,那么我们这个旋转90度的屏幕正好又错位了?答案当然不是。

当屏幕横过来的时候,我们就不旋转90度,游戏依然完美呈现。下面来说说怎么实现的。

这个实现涉及到Phaser底层源码,还有PIXI的一些机制,如果大家看不懂,可以跳过,直接看最后怎么用就行了。

首先,我们要考虑,旋转90度应该怎么做,其实只要把世界旋转90度,就可以了,但是世界旋转了之后,它是按照那个点进行旋转的,旋转之后是否要进行坐标调整?看代码。

Phaser.World.prototype.displayObjectUpdateTransform = function() {
  if(!game.scale.correct) {
    this.x = game.camera.y + game.width;
    this.y = -game.camera.x;
    this.rotation = Phaser.Math.degToRad(Phaser.Math.wrapAngle(90));
  } else {
    this.x = -game.camera.x;
    this.y = -game.camera.y;
    this.rotation = 0;
  }
 
  PIXI.DisplayObject.prototype.updateTransform.call(this);
}

注意,这里调整坐标的时候,加入了camera的修正,这是因为在世界大小比游戏区域大小大的时候,camera就不一定在(0,0)点了,所以要考虑进去。

加了这个当然不够,还需要在游戏中加入:

game.scale.onOrientationChange.add(function() {
  if(game.scale.isLandscape) {
    game.scale.correct = true;
    game.scale.setGameSize(WIDTH, HEIGHT);
  } else {
    game.scale.correct = false;
    game.scale.setGameSize(HEIGHT, WIDTH);
  }
}, this)

每一次横竖屏变化的时候,我们需要实时切换游戏的宽高,道理也是显而易见的。横屏的时候,你的游戏是1920x1080,竖屏的时候,你的游戏就应该是1080x1920,这样才能正好匹配屏幕。

怎么使用也很简单,在游戏的BootState里面加入这些代码就行,其余的事情和之前的做法一样,只是有一点提醒大家,你的game.widthgame.height是多少呢?已经不一定了,所以在元素定位的时候不要用它们去定位。我在tacit中的做法是,全局定义好游戏宽高,后面所有定位按照它来做,就不会有问题了。

好了,横屏适配就说到这里,最后给出代码地址:https://github.com/channingbreeze/games/blob/master/tacit/js/states/BootState.js


-------------------------------------------------------------------------

2017年11月29日补充

这篇文章发出来后,QQ群里就有反馈,竖屏的时候,拖动精灵,运动方向和实际方向是垂直的,试了一下,还真是,翻了翻源码,暂时没有找到特别好的解决方案,针对2.6.2版本打一个补丁。

把这段代码贴上就行了

/**
* Called by Pointer when drag starts on this Sprite. Should not usually be called directly.
*
* @method Phaser.InputHandler#startDrag
* @param {Phaser.Pointer} pointer
*/
Phaser.InputHandler.prototype.startDrag = function (pointer) {

  var x = this.sprite.x;
  var y = this.sprite.y;

  this.isDragged = true;
  this._draggedPointerID = pointer.id;

  this._pointerData[pointer.id].camX = this.game.camera.x;
  this._pointerData[pointer.id].camY = this.game.camera.y;

  this._pointerData[pointer.id].isDragged = true;

  if (this.sprite.fixedToCamera) {
    if (this.dragFromCenter) {
      var bounds = this.sprite.getBounds();

      this.sprite.cameraOffset.x = this.globalToLocalX(pointer.x) + (this.sprite.cameraOffset.x - bounds.centerX);
      this.sprite.cameraOffset.y = this.globalToLocalY(pointer.y) + (this.sprite.cameraOffset.y - bounds.centerY);
    }

    this._dragPoint.setTo(this.sprite.cameraOffset.x - pointer.x, this.sprite.cameraOffset.y - pointer.y);
  } else {
    if (this.dragFromCenter) {
        var bounds = this.sprite.getBounds();

        this.sprite.x = this.globalToLocalX(pointer.x) + (this.sprite.x - bounds.centerX);
        this.sprite.y = this.globalToLocalY(pointer.y) + (this.sprite.y - bounds.centerY);
    }

    if(game.scale.correct) {
      this._dragPoint.setTo(this.sprite.x - this.globalToLocalX(pointer.x), this.sprite.y - this.globalToLocalY(pointer.y));
    } else {
      this._dragPoint.setTo(-this.sprite.y - this.globalToLocalX(pointer.x), this.sprite.x - this.globalToLocalY(pointer.y));
    }
  }

  this.updateDrag(pointer, true);

  if (this.bringToTop) {
      this._dragPhase = true;
      this.sprite.bringToTop();
  }

  this.dragStartPoint.set(x, y);

  this.sprite.events.onDragStart$dispatch(this.sprite, pointer, x, y);

  this._pendingDrag = false;
};

/**
* Called as a Pointer actively drags this Game Object.
* 
* @method Phaser.InputHandler#updateDrag
* @private
* @param {Phaser.Pointer} pointer - The Pointer causing the drag update.
* @param {boolean} fromStart - True if this is the first update, immediately after the drag has started.
* @return {boolean}
*/
Phaser.InputHandler.prototype.updateDrag = function (pointer, fromStart) {

  if (fromStart === undefined) { fromStart = false; }

  if (pointer.isUp) {
    this.stopDrag(pointer);
    return false;
  }

  var px = this.globalToLocalX(pointer.x) + this._dragPoint.x + this.dragOffset.x;
  var py = this.globalToLocalY(pointer.y) + this._dragPoint.y + this.dragOffset.y;

  if (this.sprite.fixedToCamera) {
    if (this.allowHorizontalDrag) {
      this.sprite.cameraOffset.x = px;
    }

    if (this.allowVerticalDrag) {
      this.sprite.cameraOffset.y = py;
    }

    if (this.boundsRect) {
      this.checkBoundsRect();
    }

    if (this.boundsSprite) {
      this.checkBoundsSprite();
    }

    if (this.snapOnDrag) {
      this.sprite.cameraOffset.x = Math.round((this.sprite.cameraOffset.x - (this.snapOffsetX % this.snapX)) / this.snapX) * this.snapX + (this.snapOffsetX % this.snapX);
      this.sprite.cameraOffset.y = Math.round((this.sprite.cameraOffset.y - (this.snapOffsetY % this.snapY)) / this.snapY) * this.snapY + (this.snapOffsetY % this.snapY);
      this.snapPoint.set(this.sprite.cameraOffset.x, this.sprite.cameraOffset.y);
    }
  } else {
    var cx = this.game.camera.x - this._pointerData[pointer.id].camX;
    var cy = this.game.camera.y - this._pointerData[pointer.id].camY;

    if (this.allowHorizontalDrag) {
      if(game.scale.correct) {
        this.sprite.x = px + cx;
      } else {
        this.sprite.x = py + cy;
      }
    }

    if (this.allowVerticalDrag) {
      if(game.scale.correct) {
        this.sprite.y = py + cy;
      } else {
        this.sprite.y = -(px + cx);
      }
    }

    if (this.boundsRect) {
      this.checkBoundsRect();
    }

    if (this.boundsSprite) {
      this.checkBoundsSprite();
    }

    if (this.snapOnDrag) {
      this.sprite.x = Math.round((this.sprite.x - (this.snapOffsetX % this.snapX)) / this.snapX) * this.snapX + (this.snapOffsetX % this.snapX);
      this.sprite.y = Math.round((this.sprite.y - (this.snapOffsetY % this.snapY)) / this.snapY) * this.snapY + (this.snapOffsetY % this.snapY);
      this.snapPoint.set(this.sprite.x, this.sprite.y);
    }
  }

  this.sprite.events.onDragUpdate.dispatch(this.sprite, pointer, px, py, this.snapPoint, fromStart);

  return true;
}

示例用法见:https://github.com/channingbreeze/games/blob/master/crazybird/js/states/BootState.js


还有一个问题,使用camera去follow一个精灵的时候,也可能出现不对的情况,可以自己在update中实现一个follow就行,代码也很简单,自己调整一下camera的x和y:

CrazyBird.Bird.prototype.ownFollow = function() {
  game.camera.x = this.x - WIDTH/2;
  game.camera.y = this.y;
  if(game.camera.x > game.world.width - WIDTH) {
    game.camera.x = game.world.width - WIDTH;
  }
}

示例用法见:https://github.com/channingbreeze/games/blob/master/crazybird/js/objects/Bird.js