1 - 2012

Ваш ход, товарищ .NET, или Опять «Реверси» под nanoCAD

Андрей Грачевский

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

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

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

Рис. 1

Рис. 1

Entry level

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

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

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

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

[CommandMethod(“HelloWorld”)]

public void HelloWorld ()

{

  Editor ed = Platform.ApplicationServices.Application.DocumentManager.MdiActiveDocument.Editor;

  // Выводим в командную строку сообщение

  ed.WriteMessage(“Добро пожаловать в управляемый код nanoCAD!”);

}

И ВСЁ!

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

Структура

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

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

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

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

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

Прежде чем рисовать «Реверси», требовалось понять — что делать, за что браться.

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

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

Database db = Application.DocumentManager.MdiActiveDocument.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.ModelSpace], OpenMode.ForWrite, false) as BlockTableRecord;

  Line line = new Line();

  ObjectId lid = ms.AppendEntity(line); // добавляем в модельное пространство

  tr.AddNewlyCreatedDBObject(line, true); // и в транзакцию

  tr.Commit();  // сохраняем изменения

}

Каждый объект, добавленный в базу данных, однозначно определяется по личному коду — ObjectId. Используя ObjectId, можно открывать объекты для чтения или записи.

Создание объектов­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,­linehight), tr);

  AddVertexToMesh(mesh, new Point3d(col*gridstep,8*gridstep,linehight), tr);

  tr.Commit();

}

// создаем вершину сети

private void AddVertexToMesh(PolygonMesh PolyMesh, Point3d Pt3d, Transaction Trans)

{

  PolygonMeshVertex PMeshVer = new PolygonMeshVertex(Pt3d);

  PolyMesh.AppendVertex(PMeshVer);

  Trans.AddNewlyCreatedDBObject(PMeshVer, true);

}

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

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

Рис. 2

Рис. 2

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

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

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

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

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

Editor ed = Application.DocumentManager.MdiActiveDocument.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())

{

  // pieceId — это id перекрашиваемой фишки в БД

  // открываем объект pieceId для изменений ­ OpenMode.ForWrite

  PolygonMesh piece = myT.GetObject(this.pieceId, OpenMode.ForWrite) as PolygonMesh;

  // присваиваем цвет в зависимости от того, чья фишка

  piece.Color = (player == ePlayer.Human) ? Constants.HumanColor: Constants.PcColor;

  // подтверждаем транзакцию

  myT.Commit();

}

Вкусняшки

Для хранения игровой доски в памяти я сделал две структуры данных: массив и словарь. Массив хранит образ доски 8x8, а словарь соответствия: элемент клетки — 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();

}

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

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

Если исходники вам не нужны, а хочется просто посмотреть на «Реверси», можно скачать собранный нами модуль (http://ftp.nanocad.ru/habr/reversiMgd/MgdReversi_dll.zip).

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

Рис. 3

Рис. 3

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

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

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

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


*См.: САПР и графика № 4'2010, с. 14­15.

САПР и графика 1`2012