用haXe+NME实现水果忍者的刀光效果,支持多点触摸,Flash10, Android通用

来源:互联网 发布:如何正确对待网络暴力 编辑:程序博客网 时间:2024/04/30 09:13

前两天玩了玩水果忍者,感觉这种输入方式是非常适合多点触摸屏的,输入直观而且爽快感十足,于是就想到了如何使用haxe+NME实现刀光的效果,今天按照我的想法把效果实现了,感觉还是很逼真的。估计即使不是水果忍者中的真正算法,也差之不远了。

下面介绍一下我的算法。

算法的核心是采用NME中的Graphics.drawTriangles()来实现扭曲位图映射。对应在Flash中,这个API是从Flash10开始提供的。另外我想只要提供类似API的语言,用这个算法都没啥问题。

我看到网上有些类似的文章(http://www.j2megame.com/html/xwzx/ty/3049.html),是采用画线的方式,感觉没我这个好,我这个的算法一个优势就是,只要更换刀光的贴图,就能实现不同样式的刀光,比如五彩的,半透明的等等。

 

算法说穿了也很简单,大家看看上面的图就明白了。

图中,上面的是刀光的原始贴图,包括两端的尖锐部分,和中间的主体部分。而图中下面则是一个刀光的轨迹,其中,中间的红线是真正的触摸轨迹,每个转折点,就是捕获触摸事件TOUCH_MOVE或MOUSE_MOVE所获得的坐标点。

我们的刀光只会在屏幕上停留短暂的一点时间,也就是说,每个触摸点都是有寿命的,超过寿命的转折点,就要从轨迹中删除,从另一方面触摸点的寿命也直接决定了在该点刀光的粗细,也就是上图中每个触摸点对应的绿色线段的长度,所以我们定义触摸点数据结构如下:

class VolatilePoint {
 public var x: Float;
 public var y: Float;
 public var birth: Float;
 
 public function new(inX: Float, inY: Float) {
  x = inX;
  y = inY;
  birth = Timer.stamp(); //出生时刻以秒为单位,因为是浮点数,所以精度足够
 }
}

也就是说,触摸点中除了坐标,还记载了该点的出生时刻,我们随时可以用当前时刻减去这个出生时刻来获得它的寿命。 

每次刀光更新时(比如响应ENTER_FRAME事件),我们要遍历轨迹,把其中寿命超过最大寿命(我这里定的是0.3秒)的删掉。

轨迹也就是一个触摸点的列表,其特点是频繁在头部和尾部增删元素,这个特点特别适合用haXe的List<T>数据结构来实现,如下:

 private var track: List<VolatilePoint>;

如果想支持多点触摸实现多条刀光,那么只要维护多个track轨迹列表即可。具体请见后面的源代码。

那么上图中的绿色线的两端坐标具体是如何确定的呢?其实它们就是从触摸点出发,和当前触摸点与上一触摸点组成的线段成正负90度的两个矢量,而其长度,前面说过,应该根据触摸点的寿命按照比例计算。

设当前点为p,前一点为prev,那么绿色线两端点可如下计算:

    var angle = Math.atan2(p.y - prev.y, p.x - prev.x); // 当前点与前一点确定的直线的角度
    var len = (1 - (now - p.birth) / 0.3) * 10; // 确定刀光的半径,这里now是当前时刻,0.3是触摸点的最大寿命(秒),10则是最大刀光半径
    var x1 = p.x + len * Math.cos(angle + Math.PI / 2), y1 = p.y + len * Math.sin(angle + Math.PI / 2); //第一端点
    var x2 = p.x + len * Math.cos(angle - Math.PI / 2), y2 = p.y + len * Math.sin(angle - Math.PI / 2);  //第二端点

好了,有了这些点,我们蓝线的轮廓也就出来了,是真正绘制的刀光,你可以看到,它其实是中部的若干个扭曲矩形,加上两端尖锐部分的两个三角形组成的。

我们可以用NME的Graphics.drawTriangles()方法来一次性绘制多个三角形。刀光两端本身就是三角形,那么扭曲矩形怎么办呢?你看上图中的黄线,对,我们把每个扭曲矩形分解成两个三角形就好了。

对应的,原始贴图(图中上面部分)也类似处理,两端两个三角形,中间矩形对应分解为两个三角形,至于为什么做成4/1而不是3/1的比例,因为纹理坐标比较好表示。

然后就很简单了,所有三角形的顶点坐标以及对应的纹理坐标都可以确定了,只要简单的把它们放进数组里传递给drawTriangles()方法,让它画出来就好了,具体的代码在后面。

下面解释下drawTriangles()这个方法的参数的具体含义。

第一个参数vertices,是这一批三角形的顶点坐标,每个顶点用一对值表示,即x和y坐标,注意顶点数不一定是3的倍数,因为每个顶点可以被多个三角形共享,比如我们的例子中的矩形就是这样,4个顶点被两个三角形共用。

而第二个参数indices就是指示前面的这些顶点如何组成三角形的,indices必须是3的倍数,每三个组成一个三角形,数组中每一个元素都是前面vertices数组中的一个顶点的索引。

第三个参数是纹理坐标,它是和第一个参数vertices一一对应的,即每个顶点在纹理图片上的纹理坐标。大家只要知道在我们的原始贴图上,左上角的纹理坐标是(0, 0),而右下角则是(1, 1),那么其它点的纹理坐标就可以直接算出来了。比如最左面刀光的尖的纹理坐标就是(0, 0.5),而黄线的左端点的坐标则是(0.25, 1)。

=============================================================

刀光实现的haXe源码如下,这是支持多点触摸的版本,已经在小米手机上测试过。

 

import haxe.Timer;

import net.cnjm.haxe.events.RocTouchEvent;
import net.cnjm.haxe.game.RocGameObject;

import nme.Vector;
import nme.display.BitmapData;
import nme.display.Graphics;
import nme.events.Event;
import nme.events.TouchEvent;
import nme.geom.Rectangle;

/**
 * ...
 * @author Rocks Wang
 */

private class VolatilePoint {
 public var x: Float;
 public var y: Float;
 public var birth: Float;
 
 public function new(inX: Float, inY: Float) {
  x = inX;
  y = inY;
  birth = Timer.stamp();
 }
}

class Slash extends RocGameObject {
 
 private var tracks: Array<List<VolatilePoint>>;
 private var touched: Array<Null<Bool>>;
 private var bitmap: BitmapData;
 
 public function new() {
  super(null);
  moveTo(0, 0);
  width = 240;
  height = 400;
  
  tracks = [];
  touched = [];
  bitmap = RocUtils.loadBitmapData("res/slash.png");
  
  addEventListener(TouchEvent.TOUCH_BEGIN, onTouch);
  addEventListener(TouchEvent.TOUCH_MOVE, onTouch);
  addEventListener(TouchEvent.TOUCH_END, onTouch);
 }
 
 override public function draw(layerId: Int) {
  var gfx = canvas.acquireGraphics();
  gfx.beginFill(0x00FFFF, 0.3);
  gfx.drawRect(x, y, width, height);
  gfx.endFill();

  var now = Timer.stamp();
  var vertices: Vector<Float> = new Vector<Float>();
  var indices: Vector<Int> = new Vector<Int>();
  var uvtData: Vector<Float> = new Vector<Float>();
  var numpt = 0;
  for (track in tracks) {
   if (track == null) continue;
   var head: VolatilePoint = track.first();
   while ((head = track.first()) != null && now - head.birth > 0.3) track.pop();

   if (track.length < 4) continue;

   var prev: VolatilePoint = null, tail: VolatilePoint = track.last();
   var px1 = head.x, py1 = head.y, px2: Null<Float> = null, py2: Null<Float> = null;
   for (p in track) {
    if (prev != null) {
     var angle = Math.atan2(p.y - prev.y, p.x - prev.x) + Math.PI / 2;
     var len = (1 - (now - p.birth) / 0.3) * 10;
     var x1 = p.x + len * Math.cos(angle), y1 = p.y + len * Math.sin(angle);
     var x2 = p.x + len * Math.cos(angle + Math.PI), y2 = p.y + len * Math.sin(angle + Math.PI);
     
     if (p == tail) {
      vertices.push(p.x);
      vertices.push(p.y);
      vertices.push(px1);
      vertices.push(py1);
      vertices.push(px2);
      vertices.push(py2);
      indices.push(numpt);
      indices.push(numpt + 1);
      indices.push(numpt + 2);
      uvtData.push(1);
      uvtData.push(0.5);
      uvtData.push(0.75);
      uvtData.push(0);
      uvtData.push(0.75);
      uvtData.push(1);
      numpt += 3;
     } else if (px2 != null) { // normal
      vertices.push(x1);
      vertices.push(y1);
      vertices.push(x2);
      vertices.push(y2);
      vertices.push(px1);
      vertices.push(py1);
      vertices.push(px2);
      vertices.push(py2);
      indices.push(numpt);
      indices.push(numpt + 2);
      indices.push(numpt + 3);
      indices.push(numpt);
      indices.push(numpt + 1);
      indices.push(numpt + 3);
      uvtData.push(0.75);
      uvtData.push(0);
      uvtData.push(0.75);
      uvtData.push(1);
      uvtData.push(0.25);
      uvtData.push(0);
      uvtData.push(0.25);
      uvtData.push(1);
      numpt += 4;
     } else { // prev is head
      vertices.push(x1);
      vertices.push(y1);
      vertices.push(x2);
      vertices.push(y2);
      vertices.push(px1);
      vertices.push(py1);
      indices.push(numpt);
      indices.push(numpt + 1);
      indices.push(numpt + 2);
      uvtData.push(0.25);
      uvtData.push(0);
      uvtData.push(0.25);
      uvtData.push(1);
      uvtData.push(0);
      uvtData.push(0.5);
      numpt += 3;
     }
     px1 = x1;
     py1 = y1;
     px2 = x2;
     py2 = y2;
     
    }
    prev = p;
   }
  }
  gfx.beginBitmapFill(bitmap);
  gfx.drawTriangles(vertices, indices, uvtData);
  gfx.endFill();
 }
 
 public function onTouch(e: TouchEvent) {
  var touchId = e.touchPointID;
  //trace(">>>" + e.type + "[" + e.touchPointID + "] = (" + e.localX + "," + e.localY + "),touched=" + touched[touchId]);
  if (touched[touchId] == null) {
   touched[touchId] = false;
  }
  if (e.type == TouchEvent.TOUCH_BEGIN) {
   touched[touchId] = true;
   return; // 手指接触屏幕会同时触发TOUCH_BEGIN和TOUCH_MOVE事件,因此这里返回即可
  } else if (e.type == TouchEvent.TOUCH_END) {
   touched[touchId] = false;
  }
  if (!touched[touchId]) return;
  var track: List<VolatilePoint>;
  if ((track = tracks[touchId]) == null) {
   track = tracks[touchId] =  new List<VolatilePoint>();
  }
  var last = track.last(), dx: Float, dy: Float;
  if (last != null && (dx = e.localX - last.x) * dx + (dy = e.localY - last.y) * dy < 25) return; //防止两点距离过于接近,可能导致计算误差
  track.add(new VolatilePoint(e.localX, e.localY)); // add to end
 }
 
 override public function touchTest(inX:Float, inY:Float): Bool {
  return true;
 }
 
}

原创粉丝点击