grasshopper自定义电池开发——贪吃蛇
一、 为什么使用Visual Studio 2022和C#?
Visual Studio中拥有官方的grasshopper扩展插件,可以提供一个模板快速开发。
C#是grasshopper的原生语言,该使用语言开发扩展性,兼容性最好。
二、.如何创建一个空grasshopper项目?
a.下载并安装Visual Studio
- 如果是第一次使用Visual Studio那选用Community版本(社区版本)就可以,这三个版本中只有Community版本是可以免费使用的;
- 确保在Visual Studio安装时,在“工作负载”选项卡下选择了“桌面应用和移动应用”下的“.NET桌面开发”组件包,其组件包描述中有 .NET Framework 这个词的出现,因为这个是GH电池的依赖框架;
- 如果在之前安装Visual Studio时未选择安装上述组件包,随时可以使用Visual Studio Installer额外加装上述组件包;
b.安装扩展并使用
- 打开VS后,先点击“继续但无需代码”。进入编辑界面。
- 在最顶端的菜单栏中找到“扩展(X)”-“管理扩展(M)”,搜索Grasshopper,安装该扩展。然后需要重启VS来应用扩展。
- 新建一个项目,使用项目模板,搜索Grasshopper的模板并使用,此处使用的C#语言版本的。
4.添加新项目后,会要求输入一些电池相关的信息。如下:
1.Add-on display name :决定了这个电池在GH插件加载的时候显示的名字。
2.Component class :是C#的类的名字,与GH显示无关,这里可以使用默认值,任何不能作为C#的类名的字符都不可以出现在这里。
3.Name :是该电池在GH里会显示的名字。
4.Nickname :是该电池在GH里选择显示简称时的名字。
5.Category :是该电池在GH里会被放在的大类的名字,比如Params、Math、Sets等。GH如果本身不存在输入的大类的名字的话,GH会为这个电池单独创建一个大类。在这里我们可以填入MyComp,并且以后所有的电池都可以填相同的大类,这样方便我们找到自己创建的电池。
6.Subcategory:是该电池在GH里会被放在大类下的哪个子类里。
7.Description :是该电池在GH里显示的额外描述性信息。
c. 添加依赖
- 项目创建完成后,可以发现,引入的包
Grasshopper.Kernel、Rhino.Geometry;
变红,说明没有依赖上。
- 我们需要手动修改依赖:点击项目的顶级,会弹出项目的配置文件
进行修改。
//删除该段,因为无法找到该包
<ItemGroup>
<PackageReference Include="Grasshopper" Version="7.13.21348.13001" IncludeAssets="compile;build" />
</ItemGroup>
//替换为以下两段:若Rhino是默认安装在C盘,则可以直接使用
<ItemGroup>
<Reference Include="Grasshopper">
<HintPath>C:\Program Files\Rhino 7\Plug-ins\Grasshopper\Grasshopper.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="RhinoCommon">
<HintPath>C:\Program Files\Rhino 7\System\RhinoCommon.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
修改并保存后,可以发现,错误已经消失了。此时空项目便做好了,可以正常编译。
d. 添加GH电池目录
GH的电池需要放置在特定目录才能被加载,我们将工程目录添加到GH电池目录,GH就可以自动加载最新的电池使用。
首先打开Rhino,在命令栏里输入GrasshopperDeveloperSettings
1.把 Memory load *.GHA assemblies using COFF byte arrays 这个复选框前的钩去掉,该选项会影响我们代码的断点调试
2.点击 Add Folder
3.点击右侧的 … , 将我们刚刚的代码的存储位置下的 bin 文件夹选中,这样GH就知道去这个文件夹找到刚刚我们生成的 gha 文件了。
- 设置完成后,打开GH应该就可以找到我们刚刚创建的空插件了。
三、贪吃蛇电池
a. 功能
- 蛇吃食物能增长身体
- 设定蛇的运动平面,包括边界
- 设置x与y轴的格数
- 蛇能穿越边界并从另一端回归
- 蛇不能原地掉头
b.具体代码
类成员变量:
public sealed class Dir
{
public static readonly Dir Up = new Dir("Up");
public static readonly Dir Down = new Dir("Down");
public static readonly Dir Left = new Dir("Left");
public static readonly Dir Right = new Dir("Right");
private Dir(string value)
{
Name = value;
}
public string Name { get; private set; }
}
Point3d headPoint = new Point3d(0, 0, 0);
double headXinPlane = 0;
double headYinPlane = 0;
List<Point3d> bodyPoints = new List<Point3d>() { new Point3d(0, 0, 0) };
int seed = 0;
Circle lastFood = new Circle(1);
List<Curve> lastSnake = new List<Curve>();
int nowdirection = 0;
定义输入:
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddRectangleParameter("rectangle", "rec", "蛇运动平面", GH_ParamAccess.item);
pManager.AddIntegerParameter("xDivideNumber", "xDiv", "x轴分割的块数", GH_ParamAccess.item);
pManager.AddIntegerParameter("yDivideNumber", "yDiv", "y轴分割的块数", GH_ParamAccess.item);
pManager.AddBooleanParameter(Dir.Up.Name, Dir.Up.Name, "连接一个按钮来让蛇向+y方向转", GH_ParamAccess.item);
pManager.AddBooleanParameter(Dir.Down.Name, Dir.Down.Name, "连接一个按钮来让-y方向转", GH_ParamAccess.item);
pManager.AddBooleanParameter(Dir.Left.Name, Dir.Left.Name, "连接一个按钮来让蛇向+x方向转", GH_ParamAccess.item);
pManager.AddBooleanParameter(Dir.Right.Name, Dir.Right.Name, "连接一个按钮来让蛇向-x方向转", GH_ParamAccess.item);
pManager.AddBooleanParameter("Reset", "Reset", "连接一个按钮来重置游戏.", GH_ParamAccess.item);
}
定义输出:
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddCurveParameter("Snake", "Snake", "蛇身体的曲线", GH_ParamAccess.list);
pManager.AddCircleParameter("Food", "Food", "事物的圆", GH_ParamAccess.item);
}
逻辑:
protected override void SolveInstance(IGH_DataAccess DA)
{
//Input
bool[] dirs = { false, false, false, false };
bool zReset = false;
int xMax = 0;
int yMax = 0;
Rectangle3d rec = new Rectangle3d();
//为了避免方便设计避免掉头逻辑,使用上左下右的排列顺序
bool judge = DA.GetData(Dir.Up.Name, ref dirs[0]);
judge &= DA.GetData(Dir.Down.Name, ref dirs[2]);
judge &= DA.GetData(Dir.Left.Name, ref dirs[1]);
judge &= DA.GetData(Dir.Right.Name, ref dirs[3]);
bool judge2 = DA.GetData("Reset", ref zReset);
DA.GetData("xDivideNumber", ref xMax);
DA.GetData("yDivideNumber", ref yMax);
DA.GetData("rectangle", ref rec);
Plane plane = rec.Plane;
//rec.pointAt()使用的是百分数,故直接除以1
double xStep = 1d / xMax;
double yStep = 1d / yMax;
//间隔
double step = 0;
//半径
double r = 1;
if (xStep > yStep)
{
step = yStep;
r = yStep * rec.Height / 2;
}
else
{
step = xStep;
r = xStep * rec.Width / 2;
}
AddRuntimeMessage(GH_RuntimeMessageLevel.Remark, r + " " + xStep + " " + yStep + " " + step);
if (!judge) AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "请连接一个按钮来控制方向");
if (!judge2) AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "请连接一个按钮来复原");
AddRuntimeMessage(GH_RuntimeMessageLevel.Remark, "记得加上Trigger. 建议间隔:500ms.");
List<Curve> bodies = new List<Curve>();
Random foodRandomNumber = new Random(seed);
//Reset
if (zReset)
{
headXinPlane = 0;
headYinPlane = 0;
headPoint = rec.PointAt((headXinPlane + 0.5) * xStep, (headYinPlane + 0.5) * yStep);
bodyPoints = new List<Point3d>() { headPoint };
return;
}
//Direction
for (int i = 0; i < dirs.Length; i++)
{
if (dirs[i])
{
if (nowdirection != i && (nowdirection + 2) % dirs.Length != i)
{
//只有在往另外一个轴进行移动,才算有效移动
nowdirection = i;
}
//避免在多次输入的情况下快速刷新,只更新当前方向就返回
DA.SetData("Food", lastFood);
DA.SetDataList("Snake", lastSnake);
//return;
}
}
bodyPoints.Add(headPoint);
if (nowdirection % 2 == 0)
{
//此时是上或下
if (nowdirection == 0)
{
headYinPlane += 1;
}
else
{
headYinPlane -= 1;
}
//超出边界,从另一边回归
if (headYinPlane >= yMax)
{
headYinPlane = 0;
}
else if (headYinPlane < 0)
{
headYinPlane = yMax - 1;
}
}
else
{
if (nowdirection == 1)
{
headXinPlane += 1;
}
else
{
headXinPlane -= 1;
}
if (headXinPlane >= xMax)
{
headXinPlane = 0;
}
else if (headXinPlane < 0)
{
headXinPlane = xMax - 1;
}
}
headPoint = rec.PointAt((headXinPlane + 0.5) * xStep, (headYinPlane + 0.5) * yStep);
//Food section below
double foodRandomX = foodRandomNumber.Next(0, xMax - 1);
double foodRandomY = foodRandomNumber.Next(0, yMax - 1);
Point3d foodPoint = rec.PointAt((foodRandomX + 0.5) * xStep, (foodRandomY + 0.5) * yStep);
Circle FoodCircle = new Circle(plane, foodPoint, r);
//Eating judge
if (foodPoint.DistanceTo(headPoint) <= 2 * r) { seed++; }
else
{
bodyPoints.RemoveRange(0, 1);
}
foreach (Point3d point in bodyPoints)
{
Circle body = new Circle(plane, point, r);
bodies.Add(body.ToNurbsCurve());
}
Curve[] zSnake = Curve.CreateBooleanUnion(bodies, 1);
List<Curve> output = new List<Curve>(zSnake);
//Output
DA.SetData("Food", FoodCircle);
DA.SetDataList("Snake", output);
lastFood = FoodCircle;
lastSnake = output;
}
c.使用方法及与效果
- 使用方法
调整Trigger的间隔可以改变蛇运动的快慢
- 效果