Некоторое время назад у нас произошло большое событие — выход релиза nanoCAD 3.5. Ключевым нововведением стало открытое API, о котором и пойдет речь в этой статье.

Как известно, лучший способ что-то изучить — это сделать его. Когда-то я писал «Реверси» под nanoCAD на скрипте. Теперь решил написать на .NET. В результате получилось кросс-САПР-платфор-менное приложение, способное работать не только под nanoCAD.

Программировать под nanoCAD можно было и раньше. dows писал на скриптах кривые Серпинского, я писал «Реверси», есть и другие примеры. Все это, конечно, хорошо, но мало. Поэтому мой следующий ход — .NET.

Entry level

Первое, что нужно было сделать, — создать сборку, содержащую код, исполняемый в nanoCAD:

  • создаем проект: Visual C#, Class Library;
  • добавляем в References библиотеки .NET nanoCAD: hostdbmgd.dll, host-mgd.dll;
  • регистрируем в nanoCAD команду.

Метод, который будет регистрироваться в качестве команды, должен иметь модификатор public и быть помечен специальным атрибутом CommandMethod.

Например, HelloWorld выглядит так:

[CommandMethod("HelloWorld")]
public void HelloWorld ()
{
	Editor ed = Platform.ApplicationServices.Application.DocumentManager.MdiActiveDocument.Editor;
	// Выводим в командную строку сообщение
	ed.WriteMessage("Добро пожаловать в управляемый код nanoCAD!");
}

И ВСЁ!

Подробнее не пишу: об этом можно прочитать в nanoCAD SDK. Где взять? В Клубе разработчиков nanoCAD, регистрация открыта.

Структура

Игру я разделил на несколько классов: класс игры, класс игровой доски, класс информационной панели, класс игровой фишки:

  • класс игры должен содержать алгоритмы проверки возможности сделать ход по определенным координатам, поиска хода компьютера, подсчета фишек игроков, решения о продолжении игры;
  • класс доски — отрисовывать доску, хранить ее содержание;
  • класс информационной панели — показывать результаты прохождения партии;
  • класс фишки — отрисовывать фишку, уметь менять ее цвет, хранить всю информацию, касающуюся конкретной игровой ячейки.

Каждый класс должен быть максимально самостоятельным.

Дальше мне нужно было научиться создавать объекты, менять их и общаться с пользователем.

Создание объектов. Матчасть

Прежде чем рисовать «Реверси», требовалось понять — что делать, за что браться. Для того чтобы создать объекты, нужно немного знать о структуре документа. В каждом документе есть база данных. В базе данных хранятся объекты, содержащиеся в чертеже, и их связи друг с другом. Здесь хранится всё: и линии с дугами, и пространство модели, и стили текстов, и многое другое. Добавляя новый объект в чертеж, нужно добавить его в базу данных. А где есть база данных, там есть и транзакции.

Транзакции нужны, чтобы защитить наш документ: если в результате выполнения кода случится сбой, объекты, добавленные этим кодом, не попадут в документ — транзакция будет отменена. Если все завершилось успешно — транзакция подтверждается и объекты будут добавлены.

Database db = Application.DocumentManager. MdiActive Document.Database;
TransactionManager tm = db.TransactionManager;
using (Transaction tr = tm.StartTransaction())
{
	tr.Commit();
}

Просто создать объект мало. Он останется никуда не присоединенным, висящим в воздухе. Объект нужно куда-то поместить. Обычно это модельное пространство. В скриптах было что-то похожее (сказал модельному пространству: «Сделай линию» — и она там появится). В .NET немного по-другому: созданный объект нужно добавить в модельное пространство и в транзакцию.

using (Transaction tr = tm.StartTransaction())
{
	BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead, false) as BlockTable;
	BlockTableRecord ms = tr.GetObject(bt[BlockTableRecord.ModelS pace], OpenMode.ForWrite, false) as BlockTableRecord;
	Line line = new Line();
	ObjectId lid = ms.AppendEntity(line); // добавляем в модельное пространство tr.
	AddNewlyCreatedDBObject(line, true); // и в транзакцию
	tr.Commit(); // сохраняем изменения
}

Создание объектов-2. Полный вперед

Вооружившись знаниями о внутренней кухне документа, можно, наконец, начинать разработку класса игровой доски. Нет доски — нет и партии. Поэтому первое, что я начал делать, — стал рисовать клетки в пространстве документа.

Клетки я делал из штриховок. Открыв в NCadSDK.chm описание объекта Hatch (документация входит в SDK, доступный членам Клуба разработчиков), я почерпнул нужные мне знания. Третий абзац сразу сообщил мне, что штриховка состоит из петель, а список методов объекта штриховки подсказал магическое слово AppendLoop (). «Вот то, что мне нужно!» — подумал я.

Итак, каждую клетку я строил из квадратной полилинии, которую закрашивала штриховка. Все штриховки вместе образовывали квадрат 8 на 8 клеток.

Дальше — по накатанной, всё как в прошлый раз: бордюры и фишки создаю из объектов 3Dmesh. Бордюр — это полигон две на две вершины. Вычисляю координаты вершин, создаю их, добавляю в сеть, сеть добавляю в модель.

using (Transaction tr = tm.StartTransaction())
{
	// создаем сеть PolygonMesh mesh = new PolygonMesh(); mesh.NSize = 2; mesh.MSize = 2;
	ms.AppendEntity(mesh); tr.AddNewlyCreatedDBObject(mesh, true);
	// создаем и добавляем вершины AddVertexToMesh(mesh, new Point3d(col*gridstep, 0,-linehight), tr);
	AddVertexToMesh(mesh, new Point3d(col*gridstep, 0, linehight), tr); AddVertexToMesh(mesh, new Point3d(col*gridstep,8*gridstep,-line-hight), tr);
	AddVertexToMesh(mesh, new Point3d(col*gridstep,8*gridstep,line hight), tr);
	tr.Commit();
}
// создаем вершину сети private void
AddVertexToMesh(PolygonMesh PolyMesh, Point3d Pt3d, Transaction Trans)
{
	PolygonMeshVertex PMeshVer = new PolygonMeshVertex(Pt3d); PolyMesh.AppendVertex(PMeshVer);
	Trans.AddNewlyCreatedDBObject(PMesh Ver, true);
}

Отлично. Клетки есть, разделители есть. Нарисовать фишку теперь тоже нетрудно. Формулы вычисления координат вершин шарика я взял из скриптовой версии игры. Правда, подправил их, чтобы объект больше походил на игровую фишку «Реверси».

Что у меня получилось в результате — смотрите на рисунке.

«Я полсотни третий, выхожу на квадрат»

Теперь нужно научиться реагировать на действия пользователя.

Тут снова понадобится вспомнить мат-часть. Кроме базы данных, есть еще несколько объектов, которые относятся не к самому документу, а к приложению в целом. Это, например, объект Application — коллекция всех документов, открытых в приложении DocumentCollection. И объект взаимодействия с пользователем — Editor. Есть и другие, но их я сейчас не касаюсь.

У объекта Editor есть ряд методов для взаимодействия с пользователем: запрос объектов, запрос строки, числа, области. Запрос объекта осуществляется методом GetEntity (PromptEntityOptions). Объект PromptEntityOptions — это необязательные параметры. Через этот объект задаются строка приглашения, ключевые слова (если они нужны), выставляются ограничения на выбор объектов. Подобный объект принимают все методы ввода. Принцип хода остался прежним: пользователь выбирает клетку, куда хочет пойти. Клетка — это объект «Штриховка». Поэтому указываю, что принимаем в качестве ввода только объекты штриховки, пустой выбор — запретить, обязательно должен быть объект. И пишем строку приглашения.

Editor ed = Application.DocumentManager.MdiActive Document.Editor;
ObjectId selectObj = ObjectId.Null;
PromptEntityOptions opts = new PromptEntityOptions("Ваш ход.Укажите ячейку");
opts.SetRejectMessage("\nТолько ячейка может быть выбрана");
opts.AddAllowedClass(typeof(Hatch), false);
PromptEntityResult pr = ed.GetEntity(opts);

По клетке определяется, куда именно пользователь хочет поставить свою фишку. Далее алгоритм проверяет, можно ли это сделать. Если да — ход выполняется и нужные фишки переворачиваются.

Перекрашивание существующих фишек

Как уже сказано, все объекты живут внутри базы данных. Это значит, что для того чтобы прочитать или изменить свойства какого-либо объекта, этот объект нужно открыть. Открытие объектов происхо дит методом транзакции GetObject (). По завершении изменений транзакция подтверждается.

using (Transaction myT = db.TransactionManager.StartTransaction())
{
	// pieceld - это id перекрашиваемой фишки в БД
	// открываем объект pieceld для изменений - OpenMode.ForWrite
	PolygonMesh piece = myTGetObject(this.pieceId, OpenMode.ForWrite) as PolygonMesh;
	// присваиваем цвет в зависимости от того, чья фишка
	piece.Color = (player == ePlayer.Human) ? Constants.HumanColor : Constants.PcColor;
	// подтверждаем транзакцию myTCommit();
}

Вкусняшки

Для хранения игровой доски в памяти я сделал две структуры данных: массив и словарь. Массив хранит образ доски 8 на 8, а словарь соответствия: элемент клетки — ObjectId-штриховки. Обе структуры хранят ссылки на объекты игровой доски. При таком подходе можно не заботиться о синхронизации. Меняться будет только элемент Piece. А получить его всегда можно по ссылке. Не важно, из массива или из словаря.

Dictionary<ObjectId, Piece> GameDesc = new Dictionary<ObjectId, Piece>();
Piece[,] GameDesc_xy = new Piece[8, 8];

На .NET многие вещи мне удалось сделать красивее и проще, чем на скриптах. Возможности фрэймворка несли приятные вкусности. К примеру, с использованием LINQ структуры данных обрабатывались почти сами собой. Подсчет количества фишек пользователя — в одну строку. Выбор клетки для хода компьютера — один запрос. Красота!

int GetCounterCount(ePlayer player)
{
	// подсчет фишек игрока player
	return gamedesk.GameDesc.Where(x => x.Value.Player == player).Count();
}

Компиляция и запуск игры

Исходники игры можно взять тут: ftp.nanoCAD.ru. Откройте проект в Visual Studio или SharpDeveloper и скомпилируйте. Пути проекта настроены с расчетом на то, что nanoCAD установлен в стандартную директорию.

Если исходники вам не нужны, а хочется просто посмотреть на «Реверси», можно скачать собранный нами модуль (ftp.nanoCAD.ru.

Для запуска игры загрузите сборку MgdReversi.dll в nanoCAD командой NETLOAD. Теперь можно запускать игру командой PLAY.

Что не успел сделать

Было бы интересно, играя в nanoCAD, остановиться на середине партии, сохранить игру в файл, открыть этот файл в AutoCAD и доиграть — ведь формат файла в обеих системах один и тот же.

Но для этого понадобится переделать архитектуру приложения: сейчас информация о состоянии игры хранится в памяти команды, а нужно ее сохранять в объектах чертежа (поле, фишки), которые сохраняются в файл. Оставим это на будущее.

А до тех пор можно играть в «Реверси» не останавливаясь, от начала и до конца игры, что под AutoCAD, что под nanoCAD — и там и там игра работает одинаково. Достаточно лишь пересобрать «Реверси» под AutoCAD — используя его SDK, ObjectARX, это несложно.