守望者--AIR技术交流

标题: line fastest a* search path A星寻路 [打印本页]

作者: 破晓    时间: 2015-7-15 14:25
标题: line fastest a* search path A星寻路


http://swf.wonderfl.net/swf/usercode/4/4c/4cb6/4cb6020f3991d2a093b55298253cadf3d818a45d.swf?t=1436941223598

  1. package {
  2.     /**
  3.      * ...
  4.      * @author sliz http://game-develop.net/blog/
  5.      */
  6.     import flash.display.Bitmap;
  7.     import flash.display.BitmapData;
  8.     import flash.display.Sprite;
  9.     import flash.display.StageAlign;
  10.     import flash.display.StageScaleMode;
  11.     import flash.events.Event;
  12.     import flash.events.MouseEvent;
  13.     import flash.geom.Point;
  14.     import flash.geom.Rectangle;
  15.     import flash.text.TextField;
  16.     import flash.utils.getTimer;
  17.     import sliz.miniui.Button;
  18.     import sliz.miniui.Checkbox;
  19.     import sliz.miniui.Label;
  20.     import sliz.miniui.LabelInput;
  21.     import sliz.miniui.layouts.BoxLayout;
  22.     import sliz.miniui.Window;

  23.     public class Game2 extends Sprite {
  24.         private var _cellSize:int = 5;
  25.         private var _grid:Grid;
  26.         private var _player:Sprite;
  27.         private var _index:int;
  28.         private var _path:Array;

  29.         private var tf:Label;
  30.         private var astar:AStar;

  31.         private var path:Sprite = new Sprite();
  32.         private var image:Bitmap = new Bitmap(new BitmapData(1, 1));
  33.         private var imageWrapper:Sprite = new Sprite();

  34.         public function Game2(){
  35.             stage.align = StageAlign.TOP_LEFT;
  36.             stage.scaleMode = StageScaleMode.NO_SCALE;
  37.             addChild(imageWrapper);
  38.             imageWrapper.addChild(image);
  39.             makePlayer();

  40.             var w:Window = new Window(this, 20, 20, "tool");
  41.             numCols = new LabelInput("numCols ", "numCols");
  42.             numCols.setValue("50");
  43.             w.add(numCols);
  44.             numRows = new LabelInput("numRows ", "numRows");
  45.             w.add(numRows);
  46.             numRows.setValue("50");
  47.             cellSize = new LabelInput("cellSize", "cellSize");
  48.             cellSize.setValue("10");
  49.             w.add(cellSize);
  50.             density = new LabelInput("density ", "density");
  51.             density.setValue("0.1");
  52.             w.add(density);
  53.             isEight = new Checkbox("是否8方向");
  54.             isEight.setToggle(true);
  55.             w.add(isEight);
  56.             tf = new Label("info");
  57.             w.add(tf);
  58.             w.add(new sliz.miniui.Link("author sliz"));
  59.             w.add(new sliz.miniui.Link("source", "http://code.google.com/p/actionscriptiui/"));
  60.             var btn:Button = new Button("新建", 0, 0, null, newMap);
  61.             w.add(btn, null, 0.8);
  62.             w.setLayout(new BoxLayout(w, 1, 5));
  63.             w.doLayout();
  64.             imageWrapper.addEventListener(MouseEvent.CLICK, onGridClick);
  65.             addEventListener(Event.ENTER_FRAME, onEnterFrame);
  66.             imageWrapper.addChild(path);
  67.             makeGrid();
  68.         }

  69.         private function newMap(e:Event):void {
  70.             makeGrid();
  71.         }

  72.         private function changeMode(e:Event):void {
  73.         /*if (_grid.getType()==1) {
  74.            _grid.calculateLinks(0);
  75.            (e.currentTarget as Button).text = "  四方向  ";
  76.            }else {
  77.            _grid.calculateLinks(1);
  78.            (e.currentTarget as Button).text = "  八方向  ";
  79.          }*/
  80.         }

  81.         private function makePlayer():void {
  82.             _player = new Sprite();
  83.             _player.graphics.beginFill(0xff00ff);
  84.             _player.graphics.drawCircle(0, 0, 2);
  85.             _player.graphics.endFill();
  86.             imageWrapper.addChild(_player);
  87.         }

  88.         private function makeGrid():void {
  89.             var rows:int = int(numRows.getValue());
  90.             var cols:int = int(numCols.getValue());
  91.             _cellSize = int(cellSize.getValue());
  92.             _grid = new Grid(cols, rows);
  93.             for (var i:int = 0; i < rows * cols * Number(density.getValue()); i++){
  94.                 _grid.setWalkable(Math.floor(Math.random() * cols), Math.floor(Math.random() * rows), false);
  95.             }
  96.             _grid.setWalkable(0, 0, true);
  97.             _grid.setWalkable(cols / 2, rows / 2, false);
  98.             if (isEight.getToggle())
  99.                 _grid.calculateLinks();
  100.             else
  101.                 _grid.calculateLinks(1);
  102.             astar = new AStar(_grid);
  103.             drawGrid();
  104.             isClick = false;
  105.             _player.x = 0;
  106.             _player.y = 0;
  107.             path.graphics.clear();
  108.         }


  109.         private function drawGrid():void {
  110.             image.bitmapData = new BitmapData(_grid.numCols * _cellSize, _grid.numRows * _cellSize, false, 0xffffff);
  111.             for (var i:int = 0; i < _grid.numCols; i++){
  112.                 for (var j:int = 0; j < _grid.numRows; j++){
  113.                     var node:Node = _grid.getNode(i, j);
  114.                     if (!node.walkable){
  115.                         image.bitmapData.fillRect(new Rectangle(i * _cellSize, j * _cellSize, _cellSize, _cellSize), getColor(node));
  116.                     }
  117.                 }
  118.             }
  119.         }

  120.         private function getColor(node:Node):uint {
  121.             if (!node.walkable)
  122.                 return 0;
  123.             if (node == _grid.startNode)
  124.                 return 0xcccccc;
  125.             if (node == _grid.endNode)
  126.                 return 0xcccccc;
  127.             return 0xffffff;
  128.         }

  129.         private function onGridClick(event:MouseEvent):void {
  130.             var xpos:int = Math.floor(mouseX / _cellSize);
  131.             var ypos:int = Math.floor(mouseY / _cellSize);
  132.             xpos = Math.min(xpos, _grid.numCols - 1);
  133.             ypos = Math.min(ypos, _grid.numRows - 1);
  134.             _grid.setEndNode(xpos, ypos);

  135.             xpos = Math.floor(_player.x / _cellSize);
  136.             ypos = Math.floor(_player.y / _cellSize);
  137.             _grid.setStartNode(xpos, ypos);
  138.             findPath();
  139.             //path.graphics.clear();
  140.             //path.graphics.lineStyle(0, 0xff0000,0.5);
  141.             //path.graphics.moveTo(_player.x, _player.y);
  142.         }

  143.         private function findPath():void {
  144.             var time:int = getTimer();
  145.             if (astar.findPath()){
  146.                 _index = 0;
  147.                 isClick = true;

  148.                 astar.floyd();
  149.                 _path = astar.floydPath;
  150.                 time = getTimer() - time;
  151.                 tf.text = time + "ms   length:" + astar.path.length;
  152.                 trace(astar.floydPath);
  153.                 path.graphics.clear();
  154.                 for (var i:int = 0; i < astar.floydPath.length; i++){
  155.                     var p:Node = astar.floydPath[i];
  156.                     path.graphics.lineStyle(0, 0xff0000);
  157.                     path.graphics.drawCircle((p.x + 0.5) * _cellSize, (p.y + 0.5) * _cellSize, 2);

  158.                     path.graphics.lineStyle(0, 0xff0000, 0.5);
  159.                     path.graphics.moveTo(_player.x, _player.y);
  160.                 }
  161.             } else {
  162.                 time = getTimer() - time;
  163.                 tf.text = time + "ms 找不到";
  164.             }
  165.         }

  166.         private var isClick:Boolean = false;
  167.         private var numCols:LabelInput;
  168.         private var numRows:LabelInput;
  169.         private var cellSize:LabelInput;
  170.         private var density:LabelInput;
  171.         private var isEight:Checkbox;

  172.         private function onEnterFrame(event:Event):void {
  173.             if (!isClick){
  174.                 return;
  175.             }
  176.             var targetX:Number = _path[_index].x * _cellSize + _cellSize / 2;
  177.             var targetY:Number = _path[_index].y * _cellSize + _cellSize / 2;
  178.             var dx:Number = targetX - _player.x;
  179.             var dy:Number = targetY - _player.y;
  180.             var dist:Number = Math.sqrt(dx * dx + dy * dy);
  181.             if (dist < 1){
  182.                 _index++;
  183.                 if (_index >= _path.length){
  184.                     isClick = false;
  185.                 }
  186.             } else {
  187.                 _player.x += dx * .5;
  188.                 _player.y += dy * .5;
  189.                 path.graphics.lineTo(_player.x, _player.y);
  190.             }
  191.         }
  192.     }
  193. }

  194. import flash.geom.Point;

  195. class AStar {
  196.     //private var _open:Array;
  197.     private var _open:BinaryHeap;
  198.     private var _grid:Grid;
  199.     private var _endNode:Node;
  200.     private var _startNode:Node;
  201.     private var _path:Array;
  202.     private var _floydPath:Array;
  203.     public var heuristic:Function;
  204.     private var _straightCost:Number = 1.0;
  205.     private var _diagCost:Number = Math.SQRT2;
  206.     private var nowversion:int = 1;

  207.     public function AStar(grid:Grid){
  208.         this._grid = grid;
  209.         heuristic = euclidian2;

  210.     }

  211.     private function justMin(x:Object, y:Object):Boolean {
  212.         return x.f < y.f;
  213.     }

  214.     public function findPath():Boolean {
  215.         _endNode = _grid.endNode;
  216.         nowversion++;
  217.         _startNode = _grid.startNode;
  218.         //_open = [];
  219.         _open = new BinaryHeap(justMin);
  220.         _startNode.g = 0;
  221.         return search();
  222.     }

  223.     public function floyd():void {
  224.         if (path == null)
  225.             return;
  226.         _floydPath = path.concat();
  227.         var len:int = _floydPath.length;
  228.         if (len > 2){
  229.             var vector:Node = new Node(0, 0);
  230.             var tempVector:Node = new Node(0, 0);
  231.             floydVector(vector, _floydPath[len - 1], _floydPath[len - 2]);
  232.             for (var i:int = _floydPath.length - 3; i >= 0; i--){
  233.                 floydVector(tempVector, _floydPath[i + 1], _floydPath[i]);
  234.                 if (vector.x == tempVector.x && vector.y == tempVector.y){
  235.                     _floydPath.splice(i + 1, 1);
  236.                 } else {
  237.                     vector.x = tempVector.x;
  238.                     vector.y = tempVector.y;
  239.                 }
  240.             }
  241.         }
  242.         len = _floydPath.length;
  243.         for (i = len - 1; i >= 0; i--){
  244.             for (var j:int = 0; j <= i - 2; j++){
  245.                 if (floydCrossAble(_floydPath[i], _floydPath[j])){
  246.                     for (var k:int = i - 1; k > j; k--){
  247.                         _floydPath.splice(k, 1);
  248.                     }
  249.                     i = j;
  250.                     len = _floydPath.length;
  251.                     break;
  252.                 }
  253.             }
  254.         }
  255.     }

  256.     private function floydCrossAble(n1:Node, n2:Node):Boolean {
  257.         var ps:Array = bresenhamNodes(new Point(n1.x, n1.y), new Point(n2.x, n2.y));
  258.         for (var i:int = ps.length - 2; i > 0; i--){
  259.             if (ps[i].x>=0&&ps[i].y>=0&&ps[i].x<_grid.numCols&&ps[i].y<_grid.numRows&&!_grid.getNode(ps[i].x,ps[i].y).walkable) {
  260.                 return false;
  261.             }
  262.         }
  263.         return true;
  264.     }

  265.     private function bresenhamNodes(p1:Point, p2:Point):Array {
  266.             var steep:Boolean = Math.abs(p2.y - p1.y) > Math.abs(p2.x - p1.x);
  267.             if (steep) {
  268.                 var temp:int = p1.x;
  269.                 p1.x = p1.y;
  270.                 p1.y = temp;
  271.                 temp = p2.x;
  272.                 p2.x = p2.y;
  273.                 p2.y = temp;
  274.             }
  275.             var stepX:int = p2.x > p1.x?1:(p2.x < p1.x? -1:0);
  276.             var deltay:Number = (p2.y - p1.y)/Math.abs(p2.x-p1.x);
  277.             var ret:Array = [];
  278.             var nowX:Number = p1.x + stepX;
  279.             var nowY:Number = p1.y + deltay;
  280.             if (steep) {
  281.                 ret.push(new Point(p1.y,p1.x));
  282.             }else {
  283.                 ret.push(new Point(p1.x,p1.y));
  284.             }
  285.             if (Math.abs(p1.x - p2.x) == Math.abs(p1.y - p2.y)) {
  286.                 if(p1.x<p2.x&&p1.y<p2.y){
  287.                     ret.push(new Point(p1.x, p1.y + 1), new Point(p2.x, p2.y - 1));
  288.                 }else if(p1.x>p2.x&&p1.y>p2.y){
  289.                     ret.push(new Point(p1.x, p1.y - 1), new Point(p2.x, p2.y + 1));
  290.                 }else if(p1.x<p2.x&&p1.y>p2.y){
  291.                     ret.push(new Point(p1.x, p1.y - 1), new Point(p2.x, p2.y + 1));
  292.                 }else if(p1.x>p2.x&&p1.y<p2.y){
  293.                     ret.push(new Point(p1.x, p1.y + 1), new Point(p2.x, p2.y - 1));
  294.                 }
  295.             }
  296.             while (nowX != p2.x) {
  297.                 var fy:int=Math.floor(nowY)
  298.                 var cy:int = Math.ceil(nowY);
  299.                 if (steep) {
  300.                     ret.push(new Point(fy, nowX));
  301.                 }else{
  302.                     ret.push(new Point(nowX, fy));
  303.                 }
  304.                 if (fy != cy) {
  305.                     if (steep) {
  306.                         ret.push(new Point(cy,nowX));
  307.                     }else{
  308.                         ret.push(new Point(nowX, cy));
  309.                     }
  310.                 }else if(deltay!=0){
  311.                     if (steep) {
  312.                         ret.push(new Point(cy+1,nowX));
  313.                         ret.push(new Point(cy-1,nowX));
  314.                     }else{
  315.                         ret.push(new Point(nowX, cy+1));
  316.                         ret.push(new Point(nowX, cy-1));
  317.                     }
  318.                 }
  319.                 nowX += stepX;
  320.                 nowY += deltay;
  321.             }
  322.             if (steep) {
  323.                 ret.push(new Point(p2.y,p2.x));
  324.             }else {
  325.                 ret.push(new Point(p2.x,p2.y));
  326.             }
  327.             return ret;
  328.         }
  329.     private function floydVector(target:Node, n1:Node, n2:Node):void {
  330.         target.x = n1.x - n2.x;
  331.         target.y = n1.y - n2.y;
  332.     }

  333.     public function search():Boolean {
  334.         var node:Node = _startNode;
  335.         node.version = nowversion;
  336.         while (node != _endNode){
  337.             var len:int = node.links.length;
  338.             for (var i:int = 0; i < len; i++){
  339.                 var test:Node = node.links[i].node;
  340.                 var cost:Number = node.links[i].cost;
  341.                 var g:Number = node.g + cost;
  342.                 var h:Number = heuristic(test);
  343.                 var f:Number = g + h;
  344.                 if (test.version == nowversion){
  345.                     if (test.f > f){
  346.                         test.f = f;
  347.                         test.g = g;
  348.                         test.h = h;
  349.                         test.parent = node;
  350.                     }
  351.                 } else {
  352.                     test.f = f;
  353.                     test.g = g;
  354.                     test.h = h;
  355.                     test.parent = node;
  356.                     _open.ins(test);
  357.                     test.version = nowversion;
  358.                 }

  359.             }
  360.             if (_open.a.length == 1){
  361.                 return false;
  362.             }
  363.             node = _open.pop() as Node;
  364.         }
  365.         buildPath();
  366.         return true;
  367.     }

  368.     private function buildPath():void {
  369.         _path = [];
  370.         var node:Node = _endNode;
  371.         _path.push(node);
  372.         while (node != _startNode){
  373.             node = node.parent;
  374.             _path.unshift(node);
  375.         }
  376.     }

  377.     public function get path():Array {
  378.         return _path;
  379.     }

  380.     public function get floydPath():Array {
  381.         return _floydPath;
  382.     }

  383.     public function manhattan(node:Node):Number {
  384.         return Math.abs(node.x - _endNode.x) + Math.abs(node.y - _endNode.y);
  385.     }

  386.     public function manhattan2(node:Node):Number {
  387.         var dx:Number = Math.abs(node.x - _endNode.x);
  388.         var dy:Number = Math.abs(node.y - _endNode.y);
  389.         return dx + dy + Math.abs(dx - dy) / 1000;
  390.     }

  391.     public function euclidian(node:Node):Number {
  392.         var dx:Number = node.x - _endNode.x;
  393.         var dy:Number = node.y - _endNode.y;
  394.         return Math.sqrt(dx * dx + dy * dy);
  395.     }

  396.     private var TwoOneTwoZero:Number = 2 * Math.cos(Math.PI / 3);

  397.     public function chineseCheckersEuclidian2(node:Node):Number {
  398.         var y:int = node.y / TwoOneTwoZero;
  399.         var x:int = node.x + node.y / 2;
  400.         var dx:Number = x - _endNode.x - _endNode.y / 2;
  401.         var dy:Number = y - _endNode.y / TwoOneTwoZero;
  402.         return sqrt(dx * dx + dy * dy);
  403.     }

  404.     private function sqrt(x:Number):Number {
  405.         return Math.sqrt(x);
  406.     }

  407.     public function euclidian2(node:Node):Number {
  408.         var dx:Number = node.x - _endNode.x;
  409.         var dy:Number = node.y - _endNode.y;
  410.         return dx * dx + dy * dy;
  411.     }

  412.     public function diagonal(node:Node):Number {
  413.         var dx:Number = Math.abs(node.x - _endNode.x);
  414.         var dy:Number = Math.abs(node.y - _endNode.y);
  415.         var diag:Number = Math.min(dx, dy);
  416.         var straight:Number = dx + dy;
  417.         return _diagCost * diag + _straightCost * (straight - 2 * diag);
  418.     }
  419. }


  420. class BinaryHeap {
  421.     public var a:Array = [];
  422.     public var justMinFun:Function = function(x:Object, y:Object):Boolean {
  423.         return x < y;
  424.     };

  425.     public function BinaryHeap(justMinFun:Function = null){
  426.         a.push(-1);
  427.         if (justMinFun != null)
  428.             this.justMinFun = justMinFun;
  429.     }

  430.     public function ins(value:Object):void {
  431.         var p:int = a.length;
  432.         a[p] = value;
  433.         var pp:int = p >> 1;
  434.         while (p > 1 && justMinFun(a[p], a[pp])){
  435.             var temp:Object = a[p];
  436.             a[p] = a[pp];
  437.             a[pp] = temp;
  438.             p = pp;
  439.             pp = p >> 1;
  440.         }
  441.     }

  442.     public function pop():Object {
  443.         var min:Object = a[1];
  444.         a[1] = a[a.length - 1];
  445.         a.pop();
  446.         var p:int = 1;
  447.         var l:int = a.length;
  448.         var sp1:int = p << 1;
  449.         var sp2:int = sp1 + 1;
  450.         while (sp1 < l){
  451.             if (sp2 < l){
  452.                 var minp:int = justMinFun(a[sp2], a[sp1]) ? sp2 : sp1;
  453.             } else {
  454.                 minp = sp1;
  455.             }
  456.             if (justMinFun(a[minp], a[p])){
  457.                 var temp:Object = a[p];
  458.                 a[p] = a[minp];
  459.                 a[minp] = temp;
  460.                 p = minp;
  461.                 sp1 = p << 1;
  462.                 sp2 = sp1 + 1;
  463.             } else {
  464.                 break;
  465.             }
  466.         }
  467.         return min;
  468.     }
  469. }

  470. class Grid {

  471.     private var _startNode:Node;
  472.     private var _endNode:Node;
  473.     private var _nodes:Array;
  474.     private var _numCols:int;
  475.     private var _numRows:int;

  476.     private var type:int;

  477.     private var _straightCost:Number = 1.0;
  478.     private var _diagCost:Number = Math.SQRT2;

  479.     public function Grid(numCols:int, numRows:int){
  480.         _numCols = numCols;
  481.         _numRows = numRows;
  482.         _nodes = new Array();

  483.         for (var i:int = 0; i < _numCols; i++){
  484.             _nodes[i] = new Array();
  485.             for (var j:int = 0; j < _numRows; j++){
  486.                 _nodes[i][j] = new Node(i, j);
  487.             }
  488.         }
  489.     }

  490.     /**
  491.      *
  492.      * @param    type    0四方向 1八方向 2跳棋
  493.      */
  494.     public function calculateLinks(type:int = 0):void {
  495.         this.type = type;
  496.         for (var i:int = 0; i < _numCols; i++){
  497.             for (var j:int = 0; j < _numRows; j++){
  498.                 initNodeLink(_nodes[i][j], type);
  499.             }
  500.         }
  501.     }

  502.     public function getType():int {
  503.         return type;
  504.     }

  505.     /**
  506.      *
  507.      * @param    node
  508.      * @param    type    0八方向 1四方向 2跳棋
  509.      */
  510.     private function initNodeLink(node:Node, type:int):void {
  511.         var startX:int = Math.max(0, node.x - 1);
  512.         var endX:int = Math.min(numCols - 1, node.x + 1);
  513.         var startY:int = Math.max(0, node.y - 1);
  514.         var endY:int = Math.min(numRows - 1, node.y + 1);
  515.         node.links = [];
  516.         for (var i:int = startX; i <= endX; i++){
  517.             for (var j:int = startY; j <= endY; j++){
  518.                 var test:Node = getNode(i, j);
  519.                 if (test == node || !test.walkable){
  520.                     continue;
  521.                 }
  522.                 if (type != 2 && i != node.x && j != node.y){
  523.                     var test2:Node = getNode(node.x, j);
  524.                     if (!test2.walkable){
  525.                         continue;
  526.                     }
  527.                     test2 = getNode(i, node.y);
  528.                     if (!test2.walkable){
  529.                         continue;
  530.                     }
  531.                 }
  532.                 var cost:Number = _straightCost;
  533.                 if (!((node.x == test.x) || (node.y == test.y))){
  534.                     if (type == 1){
  535.                         continue;
  536.                     }
  537.                     if (type == 2 && (node.x - test.x) * (node.y - test.y) == 1){
  538.                         continue;
  539.                     }
  540.                     if (type == 2){
  541.                         cost = _straightCost;
  542.                     } else {
  543.                         cost = _diagCost;
  544.                     }
  545.                 }
  546.                 node.links.push(new Link(test, cost));
  547.             }
  548.         }
  549.     }

  550.     public function getNode(x:int, y:int):Node {
  551.         return _nodes[x][y];
  552.     }

  553.     public function setEndNode(x:int, y:int):void {
  554.         _endNode = _nodes[x][y];
  555.     }

  556.     public function setStartNode(x:int, y:int):void {
  557.         _startNode = _nodes[x][y];
  558.     }

  559.     public function setWalkable(x:int, y:int, value:Boolean):void {
  560.         _nodes[x][y].walkable = value;
  561.     }

  562.     public function get endNode():Node {
  563.         return _endNode;
  564.     }

  565.     public function get numCols():int {
  566.         return _numCols;
  567.     }

  568.     public function get numRows():int {
  569.         return _numRows;
  570.     }

  571.     public function get startNode():Node {
  572.         return _startNode;
  573.     }

  574. }

  575. class Link {
  576.     public var node:Node;
  577.     public var cost:Number;

  578.     public function Link(node:Node, cost:Number){
  579.         this.node = node;
  580.         this.cost = cost;
  581.     }

  582. }

  583. class Node {
  584.     public var x:int;
  585.     public var y:int;
  586.     public var f:Number;
  587.     public var g:Number;
  588.     public var h:Number;
  589.     public var walkable:Boolean = true;
  590.     public var parent:Node;
  591.     //public var costMultiplier:Number = 1.0;
  592.     public var version:int = 1;
  593.     public var links:Array;

  594.     //public var index:int;
  595.     public function Node(x:int, y:int){
  596.         this.x = x;
  597.         this.y = y;
  598.     }

  599.     public function toString():String {
  600.         return "x:" + x + " y:" + y;
  601.     }
  602. }
复制代码



[attach]1221[/attach]


本文来自:http://wonderfl.net/c/aWCe

作者: lxz    时间: 2019-2-24 02:30
感谢分享!~




欢迎光临 守望者--AIR技术交流 (http://www.airmyth.com/)