码迷,mamicode.com
首页 > 编程语言 > 详细

unity简单设计模式---CoroutineScheduler

时间:2015-03-15 23:04:33      阅读:215      评论:0      收藏:0      [点我收藏+]

标签:singleton   broadcast   delegate   设计模式   

内容

描述:

        一个简单的 coroutine scheduler 协同调度程序。这个 coroutine scheduler 协同调度程序 允许执行制度的协同程序集的完全控制权。阅读代码也将帮助您了解如何协同工作在幕后。了解如何在协同的基础上构建 .Net generators 将允许您将协同支持添加到非unity项目。

协同可以yield 直到下次更新 "yield;",   直至更新给定的数目已通过"yield anInt;",  直到给定的秒数已通过"yield aFloat;",  或另一个协同已完成"yield scheduler.StartCoroutine(Coroutine());".

多个scheduler 程序实例被支持,并且可以非常有用。协同运行在一个scheduler 调度程序可以运行在一个完全不同的scheduler 程序实例下的协同yield (wait)

        因为我不能访问其内部的数据所需的scheduling不使用unity的 YieldInstruction 类。 Semantics 语义学是从 unity 的Unity‘s调度程序略有不同。例如,在 Unity 中如果你开始协同它将运行对其第一次的yield 立即,虽然在此scheduler 程序,它将不会运行直到下一次调用 UpdateAllCoroutines。此功能允许在任何时候,同时确保开始协同程序只能运行在特定的时间启动协同程序的任何代码。

你不应该依靠更新order 订单之间协同程序运行在相同的更新。

为更深入地了解和学会更多关于协同程序如何实现作为state machines  。看post 在引述的文章.

使用

创建和驾驶 CoroutineSchedulers 很容易,而且最好的示例所示。



var scheduler = new CoroutineScheduler();
 
function Update() {
  scheduler.UpdateAllCoroutines(Time.frameCount, Time.time);
}

您通常不会使用unity 的 Time.frameCount 或 Time.time 。作为在这种情况下你可能以及使用unity 的内置scheduler调度程序,但它更容易理解的例子。

Starting 开始协同和 yielding 执行,也最好是通过具体例子说明的。

scheduler.StartCoroutine(MyCoroutine());
 
function MyCoroutine() : IEnumerator {
  print("MyCoroutine: Begin");
  yield; // wait for next update
  print("MyCoroutine: next update;");
  yield 2; // wait for 2 updates, same as yield; yield;
  print("MyCoroutine: After yield 2;");
  yield 3.5; // wait for 3.5 seconds
  print("MyCoroutine: After 3.5 seconds;");
  // you can also yield for a coroutine running on a completely different scheduler instance
  yield scheduler.StartCoroutine(WaitForMe());
  print("MyCoroutine: After WaitForMe() finished;");
}
 
function WaitForMe() {
  yield 7.8; // wait for 7.8 seconds before finishing
}

在此特定示例不使用的功能的详细信息,请阅读文档注释。

 代码

CoroutineScheduler.js

#pragma strict
 
/**
 * A simple coroutine scheduler. Coroutines can yield until the next update
 * "yield;", until a given number of updates "yield anInt", until a given
 * amount of seconds "yield aFloat;", or until another coroutine has finished
 * "yield scheduler.StartCoroutine(Coroutine())".
 *
 * Multiple scheduler instances are supported and can be very useful. A
 * coroutine running under one scheduler can yield (wait) for a coroutine
 * running under a completely different scheduler instance.
 *
 * Unity‘s YieldInstruction classes are not used because I cannot
 * access their internal data needed for scheduling. Semantics are slightly
 * different from Unity‘s scheduler. For example, in Unity if you start a
 * coroutine it will run up to its first yield immediately, while in this
 * scheduler it will not run until the next time UpdateAllCoroutines is called.
 * This feature allows any code to start coroutines at any time, while
 * making sure the started coroutines only run at specific times.
 *
 * You should not depend on update order between coroutines running on the same
 * update. For example, StartCoroutine(A), StartCoroutine(B), StartCoroutine(C)
 * where A, B, C => while(true) { print(A|B|C); yield; }, do not expect "ABC" or
 * "CBA" or any other specific ordering.
 */
class CoroutineScheduler {
  /**
   * The first node in list of the coroutines that are scheduled for execution.
   */
  var first : CoroutineNode = null;
  var currentFrame : int;
  var currentTime : float;
 
  /**
   * Starts a coroutine, the coroutine does not run immediately but on the
   * next call to UpdateAllCoroutines. The execution of a coroutine can
   * be paused at any point using the yield statement. The yield return value
   * specifies when the coroutine is resumed.
   */
  function StartCoroutine(fiber : IEnumerator) : CoroutineNode {
    // if function does not have a yield, fiber will be null and we no-op
    if (fiber == null) { return; }
    // create coroutine node and run until we reach first yield
    var coroutine = new CoroutineNode(fiber);
    AddCoroutine(coroutine);
    return coroutine;
  }
 
  /**
   * Stops all coroutines running on this behaviour. Use of this method is
   * discouraged, think of a natural way for your coroutines to finish
   * on their own instead of being forcefully stopped before they finish.
   * If you need finer control over stopping coroutines you can use multiple
   * schedulers.
   */
  function StopAllCoroutines() {
    first = null;
  }
 
  /**
   * Returns true if this scheduler has any coroutines. You can use this to
   * check if all coroutines have finished or been stopped.
   */
  function HasCoroutines() : boolean {
    return first != null;
  }
 
  /**
   * Runs all active coroutines until their next yield. Caller must provide
   * the current frame and time. This allows for schedulers to run under
   * frame and time regimes other than the Unity‘s main game loop.
   */
  function UpdateAllCoroutines(frame : int, time : float) {
    currentFrame = frame;
    currentTime = time;
    var coroutine = first;
    while (coroutine != null) {
      // store listNext before coroutine finishes and is removed from the list
      var listNext = coroutine.listNext;
      if (coroutine.waitForFrame > 0 && frame >= coroutine.waitForFrame) {
        coroutine.waitForFrame = -1;
        UpdateCoroutine(coroutine);
      } else if (coroutine.waitForTime > 0.0 && time >= coroutine.waitForTime) {
        coroutine.waitForTime = -1.0;
        UpdateCoroutine(coroutine);
      } else if (coroutine.waitForCoroutine &&
                 coroutine.waitForCoroutine.finished) {
        coroutine.waitForCoroutine = null;
        UpdateCoroutine(coroutine);
      } else if (coroutine.waitForFrame == -1 &&
                 coroutine.waitForTime == -1.0 &&
                 coroutine.waitForCoroutine == null) { // initial update
        UpdateCoroutine(coroutine);
      }
      coroutine = listNext;
    }
  }
 
  /**
   * Executes coroutine until next yield. If coroutine has finished, flags
   * it as finished and removes it from scheduler list.
   */
  private function UpdateCoroutine(coroutine : CoroutineNode) {
    var fiber = coroutine.fiber;
    if (coroutine.fiber.MoveNext()) {
      var yieldCommand : Object = (fiber.Current == null) ? 1 : fiber.Current;
      if (yieldCommand instanceof int) {
        coroutine.waitForFrame = yieldCommand;
        coroutine.waitForFrame += currentFrame;
      } else if (yieldCommand instanceof float) {
        coroutine.waitForTime = yieldCommand;
        coroutine.waitForTime +=  currentTime;
      } else if  (yieldCommand instanceof CoroutineNode) {
        coroutine.waitForCoroutine = yieldCommand;
      } else {
        throw "Unexpected coroutine yield type: " + yieldCommand.GetType();
      }
    } else { // coroutine finished
      coroutine.finished = true;
      RemoveCoroutine(coroutine);
    }
  }
 
  private function AddCoroutine(coroutine : CoroutineNode) {
    if (first != null) {
      coroutine.listNext = first;
      first.listPrevious = coroutine;
    }
    first = coroutine;
  }
 
  private function RemoveCoroutine(coroutine : CoroutineNode) {
    if (first == coroutine) { // remove first
      first = coroutine.listNext;
    } else { // not head of list
      if (coroutine.listNext != null) { // remove between
        coroutine.listPrevious.listNext = coroutine.listNext;
        coroutine.listNext.listPrevious = coroutine.listPrevious;
      } else if (coroutine.listPrevious != null) { // and listNext is null
        coroutine.listPrevious.listNext = null; // remove last
      }
    }
    coroutine.listPrevious = null;
    coroutine.listNext = null;
  }
}

CoroutineNode.js

#pragma strict
 
/**
 * Linked list node type used by coroutine scheduler to track scheduling of
 * coroutines.
 */
class CoroutineNode {
  var listPrevious : CoroutineNode = null;
  var listNext : CoroutineNode = null;
  var fiber : IEnumerator;
  var finished = false;
  var waitForFrame = -1;
  var waitForTime = -1.0;
  var waitForCoroutine : CoroutineNode = null;
 
  function CoroutineNode(fiber : IEnumerator) {
    this.fiber = fiber;
  }
}

Ported to CSharp

CoroutineSchedulerTest.cs

using UnityEngine;
using System.Collections;
 
 
/// <summary>
/// CoroutineSchedulerTest.cs
/// 
/// Port of the Javascript version from 
/// http://www.unifycommunity.com/wiki/index.php?title=CoroutineScheduler
/// 
/// Linked list node type used by coroutine scheduler to track scheduling of coroutines.
///  
/// BMBF Researchproject http://playfm.htw-berlin.de
/// PlayFM - Serious Games für den IT-gestützten Wissenstransfer im Facility Management 
///	Gef?rdert durch das bmb+f - Programm Forschung an Fachhochschulen profUntFH
///	
///	<author>Frank.Otto@htw-berlin.de</author>
///
/// </summary>
 
public class CoroutineSchedulerTest : MonoBehaviour
{
 
	CoroutineScheduler scheduler;
	// Use this for initialization
	void Start ()
	{
		scheduler = new CoroutineScheduler ();
		scheduler.StartCoroutine (MyCoroutine ());
	}
 
 
	IEnumerator MyCoroutine ()
	{
		Debug.Log ("MyCoroutine: Begin");
		yield return 0;
		// wait for next update
		Debug.Log ("MyCoroutine: next update;" + Time.time);
		yield return 2;
		// wait for 2 updates, same as yield; yield;
		Debug.Log ("MyCoroutine: After yield 2;" + Time.time);
		yield return 3.5f;
		// wait for 3.5 seconds
		Debug.Log ("MyCoroutine: After 3.5 seconds;" + Time.time);
		// you can also yield for a coroutine running on a completely different scheduler instance
		yield return scheduler.StartCoroutine (WaitForMe ());
		Debug.Log ("MyCoroutine: After WaitForMe() finished;" + Time.time);
	}
 
	IEnumerator WaitForMe ()
	{
		yield return 7.8f;
		// wait for 7.8 seconds before finishing
	}
 
	// Update is called once per 	
	void Update ()
	{
 
		scheduler.UpdateAllCoroutines (Time.frameCount, Time.time);
	}
 
 
}

CoroutineScheduler.cs

using System.Collections;
using UnityEngine;
 
/// <summary>
/// CoroutineScheduler.cs
/// 
/// Port of the Javascript version from 
/// http://www.unifycommunity.com/wiki/index.php?title=CoroutineScheduler
/// 
/// Linked list node type used by coroutine scheduler to track scheduling of coroutines.
/// 
/// 
/// BMBF Researchproject http://playfm.htw-berlin.de
/// PlayFM - Serious Games für den IT-gestützten Wissenstransfer im Facility Management 
///	Gef?rdert durch das bmb+f - Programm Forschung an Fachhochschulen profUntFH
///	
///	<author>Frank.Otto@htw-berlin.de</author>
///
/// 
/// A simple coroutine scheduler. Coroutines can yield until the next update
/// "yield;", until a given number of updates "yield anInt", until a given
/// amount of seconds "yield aFloat;", or until another coroutine has finished
/// "yield scheduler.StartCoroutine(Coroutine())".
/// 
/// Multiple scheduler instances are supported and can be very useful. A
/// coroutine running under one scheduler can yield (wait) for a coroutine
/// running under a completely different scheduler instance.
/// 
/// Unity‘s YieldInstruction classes are not used because I cannot
/// access their internal data needed for scheduling. Semantics are slightly
/// different from Unity‘s scheduler. For example, in Unity if you start a
/// coroutine it will run up to its first yield immediately, while in this
/// scheduler it will not run until the next time UpdateAllCoroutines is called.
/// This feature allows any code to start coroutines at any time, while
/// making sure the started coroutines only run at specific times.
/// 
/// You should not depend on update order between coroutines running on the same
/// update. For example, StartCoroutine(A), StartCoroutine(B), StartCoroutine(C)
/// where A, B, C => while(true) { print(A|B|C); yield; }, do not expect "ABC" or
/// "CBA" or any other specific ordering.
/// </summary>
public class CoroutineScheduler : MonoBehaviour
{
 
	CoroutineNode first = null;
	int currentFrame;
	float currentTime;
 
	/**
   * Starts a coroutine, the coroutine does not run immediately but on the
   * next call to UpdateAllCoroutines. The execution of a coroutine can
   * be paused at any point using the yield statement. The yield return value
   * specifies when the coroutine is resumed.
   */
 
	public CoroutineNode StartCoroutine (IEnumerator fiber)
	{
		// if function does not have a yield, fiber will be null and we no-op
		if (fiber == null) {
			return null;
		}
		// create coroutine node and run until we reach first yield
		CoroutineNode coroutine = new CoroutineNode (fiber);
		AddCoroutine (coroutine);
		return coroutine;
	}
 
	/**
   * Stops all coroutines running on this behaviour. Use of this method is
   * discouraged, think of a natural way for your coroutines to finish
   * on their own instead of being forcefully stopped before they finish.
   * If you need finer control over stopping coroutines you can use multiple
   * schedulers.
   */
	public void StopAllCoroutines ()
	{
		first = null;
	}
 
	/**
   * Returns true if this scheduler has any coroutines. You can use this to
   * check if all coroutines have finished or been stopped.
   */
	public bool HasCoroutines ()
	{
		return first != null;
	}
 
	/**
   * Runs all active coroutines until their next yield. Caller must provide
   * the current frame and time. This allows for schedulers to run under
   * frame and time regimes other than the Unity‘s main game loop.
   */
	public void UpdateAllCoroutines (int frame, float time)
	{
		currentFrame = frame;
		currentTime = time;
		CoroutineNode coroutine = this.first;
		while (coroutine != null) {
			// store listNext before coroutine finishes and is removed from the list
			CoroutineNode listNext = coroutine.listNext;
 
			if (coroutine.waitForFrame > 0 && frame >= coroutine.waitForFrame) {
				coroutine.waitForFrame = -1;
				UpdateCoroutine (coroutine);
			} else if (coroutine.waitForTime > 0.0f && time >= coroutine.waitForTime) {
				coroutine.waitForTime = -1.0f;
				UpdateCoroutine (coroutine);
			} else if (coroutine.waitForCoroutine != null && coroutine.waitForCoroutine.finished) {
				coroutine.waitForCoroutine = null;
				UpdateCoroutine (coroutine);
			} else if (coroutine.waitForFrame == -1 && coroutine.waitForTime == -1.0f && coroutine.waitForCoroutine == null) {
				// initial update
				UpdateCoroutine (coroutine);
			}
			coroutine = listNext;
		}
	}
 
	/**
   * Executes coroutine until next yield. If coroutine has finished, flags
   * it as finished and removes it from scheduler list.
   */
	private void UpdateCoroutine (CoroutineNode coroutine)
	{
		IEnumerator fiber = coroutine.fiber;
		if (coroutine.fiber.MoveNext ()) {
			System.Object yieldCommand = fiber.Current == null ? (System.Object) 1 : fiber.Current;
 
			if (yieldCommand.GetType () == typeof(int)) {
				coroutine.waitForFrame = (int) yieldCommand;
				coroutine.waitForFrame += (int) currentFrame;
			} else if (yieldCommand.GetType () == typeof(float)) {
				coroutine.waitForTime = (float) yieldCommand;
				coroutine.waitForTime += (float) currentTime;
			} else if (yieldCommand.GetType () == typeof(CoroutineNode)) {
				coroutine.waitForCoroutine = (CoroutineNode) yieldCommand;
			} else {
				throw new System.ArgumentException ("CoroutineScheduler: Unexpected coroutine yield type: " + yieldCommand.GetType ());
			}
		} else {
			// coroutine finished
			coroutine.finished = true;
			RemoveCoroutine (coroutine);
		}
	}
 
	private void AddCoroutine (CoroutineNode coroutine)
	{
 
		if (this.first != null) {
			coroutine.listNext = this.first;
			first.listPrevious = coroutine;
		}
		first = coroutine;
	}
 
	private void RemoveCoroutine (CoroutineNode coroutine)
	{
		if (this.first == coroutine) {
			// remove first
			this.first = coroutine.listNext;
		} else {
			// not head of list
			if (coroutine.listNext != null) {
				// remove between
				coroutine.listPrevious.listNext = coroutine.listNext;
				coroutine.listNext.listPrevious = coroutine.listPrevious;
			} else if (coroutine.listPrevious != null) {
				// and listNext is null
				coroutine.listPrevious.listNext = null;
				// remove last
			}
		}
		coroutine.listPrevious = null;
		coroutine.listNext = null;
	}
 
}//class

CoroutineNode.cs

using System.Collections;
using UnityEngine;
 
 
/// <summary>
/// CoroutineNode.cs
/// 
/// Port of the Javascript version from 
/// http://www.unifycommunity.com/wiki/index.php?title=CoroutineScheduler
/// 
/// Linked list node type used by coroutine scheduler to track scheduling of coroutines.
///  
/// BMBF Researchproject http://playfm.htw-berlin.de
/// PlayFM - Serious Games für den IT-gestützten Wissenstransfer im Facility Management 
///	Gef?rdert durch das bmb+f - Programm Forschung an Fachhochschulen profUntFH
///	
///	<author>Frank.Otto@htw-berlin.de</author>
///
/// </summary>
 
public class CoroutineNode
{
	public CoroutineNode listPrevious = null;
	public CoroutineNode listNext = null;
	public IEnumerator fiber;
	public bool finished = false;
	public int waitForFrame = -1;
	public float waitForTime = -1.0f;
	public CoroutineNode waitForCoroutine;
 
	public CoroutineNode (IEnumerator _fiber)
	{
		this.fiber = _fiber;
	}
}

Additional Implementation C#

如果您想要使用 CoroutineScheduler 与 Yield  的命令从 UnityEngine 命名空间内这里是 添加实现, 我发现它有用consuming  api 的类定义之外的 UnityEngine 命名空间中。

Sample Usage

m_scheduler = new CoroutineScheduler();
m_scheduler.StartCoroutine(testAPI());
 
IEnumerator testAPI()
{
   // ...set up request
 
   var www = new UnityEngine.WWW(requestURL);
   yield return new UnityWWWYieldWrapper(www);
 
   // ...loading complete do some stuff
}

Addition to CoroutineScheduler.cs

//line 110
 
public void UpdateAllCoroutines(int frame, float time)
{
   currentFrame = frame;
   currentTime = time;
   CoroutineNode coroutine = this.first;
   while (coroutine != null)
   {
      // store listNext before coroutine finishes and is removed from the list
      CoroutineNode listNext = coroutine.listNext;
 
      if (coroutine.waitForFrame > 0 && frame >= coroutine.waitForFrame)
      {
         coroutine.waitForFrame = -1;
         UpdateCoroutine(coroutine);
      }
      else if (coroutine.waitForTime > 0.0f && time >= coroutine.waitForTime)
      {
         coroutine.waitForTime = -1.0f;
         UpdateCoroutine(coroutine);
      }
      else if (coroutine.waitForCoroutine != null && coroutine.waitForCoroutine.finished)
      {
         coroutine.waitForCoroutine = null;
         UpdateCoroutine(coroutine);
      }
      else if (coroutine.waitForUnityObject != null && coroutine.waitForUnityObject.finished)//lonewolfwilliams
      {
         coroutine.waitForUnityObject = null;
         UpdateCoroutine(coroutine);
      }
      else if (coroutine.waitForFrame == -1 && coroutine.waitForTime == -1.0f 
               && coroutine.waitForCoroutine == null && coroutine.waitForUnityObject == null)
      {
         // initial update
         UpdateCoroutine(coroutine);
      }
      coroutine = listNext;
   }
}
 
//line 154
 
private void UpdateCoroutine(CoroutineNode coroutine)
{
   IEnumerator fiber = coroutine.fiber;
   if (coroutine.fiber.MoveNext())
   {
      System.Object yieldCommand = fiber.Current == null ? (System.Object)1 : fiber.Current;
 
      if (yieldCommand.GetType() == typeof(int))
      {
         coroutine.waitForFrame = (int)yieldCommand;
         coroutine.waitForFrame += (int)currentFrame;
      }
      else if (yieldCommand.GetType() == typeof(float))
      {
         coroutine.waitForTime = (float)yieldCommand;
         coroutine.waitForTime += (float)currentTime;
      }
      else if (yieldCommand.GetType() == typeof(CoroutineNode))
      {
         coroutine.waitForCoroutine = (CoroutineNode)yieldCommand;
      }
      else if (yieldCommand is IYieldWrapper) //lonewolfwilliams
      {
         coroutine.waitForUnityObject = yieldCommand as IYieldWrapper;
      }
      else
      {
         throw new System.ArgumentException("CoroutineScheduler: Unexpected coroutine yield type: " + yieldCommand.GetType());
 
         //this is an alternative if you don‘t have access to the function passed to the couroutineScheduler - maybe it‘s                      
         //precompiled in a dll for example - remember you will have to add a case every time you add a wrapper :/
         /*
         var commandType = yieldCommand.GetType();
	 if(commandType == typeof(UnityEngine.WWW))
         {
	    coroutine.waitForUnityObject = 
               new UnityWWWWrapper(yieldCommand as UnityEngine.WWW);
	 }
	 else if(commandType == typeof(UnityEngine.AsyncOperation))
	 {
	    coroutine.waitForUnityObject = 
	       new UnityASyncOpWrapper(yieldCommand as UnityEngine.AsyncOperation);
	 }
	 else if(commandType == typeof(UnityEngine.AssetBundleRequest))
	 {
	    coroutine.waitForUnityObject = 
	       new UnityAssetBundleRequestWrapper(yieldCommand as UnityEngine.AssetBundleRequest);
	 }
	 else
	 {
            throw new System.ArgumentException("CoroutineScheduler: Unexpected coroutine yield type: " + yieldCommand.GetType());
	 }
         */
      }
   }
   else
   {
      // coroutine finished
      coroutine.finished = true;
      RemoveCoroutine(coroutine);
   }
}

Addition to CoroutineNode.cs

public class CoroutineNode
{
   public CoroutineNode listPrevious = null;
   public CoroutineNode listNext = null;
   public IEnumerator fiber;
   public bool finished = false;
   public int waitForFrame = -1;
   public float waitForTime = -1.0f;
   public CoroutineNode waitForCoroutine;
   public IYieldWrapper waitForUnityObject; //lonewolfwilliams
 
   public CoroutineNode(IEnumerator _fiber)
   {
      this.fiber = _fiber;
   }
}

IYieldWrapper.cs

您将需要编写您自己的实现 yield 命令从 UnityEngine 命名空间中,此接口应该帮助有助于实现这一点。

/*
 * gareth williams 
 * http://www.lonewolfwilliams.com
 */
 
public interface IYieldWrapper
{
   bool finished { get; }
}

Example Wrappers

以下是一些我使用的wrappers ,事实上他们有几乎完全相同的 signatures 签名,所以一个更通用的实现可能可以被写入 ^_^

UnityASyncOpWrapper

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
/*
*   Gareth Williams
*   http://www.lonewolfwilliams.com
*/
 
class UnityASyncOpWrapper : IYieldWrapper
{
   private UnityEngine.AsyncOperation m_UnityObject;
   public bool finished
   {
      get
      {
         return m_UnityObject.isDone;
      }
   }
 
   public UnityASyncOpWrapper(UnityEngine.AsyncOperation wraps)
   {
      m_UnityObject = wraps;
   }
}

UnityWWWYieldWrapper

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
/*
 * Gareth Williams
 * http://www.lonewolfwilliams.com
 */
 
public class UnityWWWYieldWrapper : IYieldWrapper
{
   private UnityEngine.WWW m_UnityObject;
   public bool finished
   {
      get
      {
         return m_UnityObject.isDone;
      }
   }
 
   public UnityWWWYieldWrapper(UnityEngine.WWW wraps)
   {
      m_UnityObject = wraps;
   }
}
??

unity简单设计模式---CoroutineScheduler

标签:singleton   broadcast   delegate   设计模式   

原文地址:http://blog.csdn.net/u010019717/article/details/44183319

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!