|
|
|
Канал Игры Мечты: «.Net скрываем конечный автомат или "бот v2.0"» |
|
|
Варсик 545 EGP
Рейтинг канала: 4(81) Репутация: 117 Сообщения: 4041 Откуда: Москва Зарегистрирован: 22.12.2002 |
|
Предисловие
Не так давно в КСО проскакивал разговор по поводу того как взломать игрушку и написать бота для нее. Результатом стал удачный взлом и я получил возможность писать ботов используя обстракции клиента. Однако бот получался сложный, из-за особенностей написания... Сейчас объясню подробнее...
Итак... Что я получил на входе... Каждый фреим (перерисовка экрана) я получал управление и мог использовать обращаться к любому примитиву внутри игры. Скрипт выглядел как конечный автомат. Программирование автоматов - это отдельная наука и почитать про это можно в Вики... Скажите, сколько народу умеют программировать конечные автоматы или машины состояний?
Этому учат в институте, но мало и вскользь, а практики нету вообще никакой. Бота-то я написал, но его дебаг стал сложен и трудоемок. И я нашел выход...
Потоки, которые не потоки
Самое простое, что можно было сделать: Запускать на каждый скрипт свой поток, который был-бы в постоянном синхроне. То есть... Когда выполняется операция со временем "лететь туда-то", то поток ждет выполнения этой операции, а машина состояний проверяет - закончилась-ли эта операция. Как только закончилась, поток получает управление, а основная программа ждет, пока поток разберется что и где ему надо сделать. Фактически принудительный свитч потока 2 раза с синхронизацией. При 60 FPS это ОЧЕНЬ дорогая реализация.
Итераторы и сахар
А потом я нашел конструкцию yield return. Подробности по ее поводу тут: http://msdn.microsoft.com/ru-ru/library/9k7k7cf0.aspx Если коротко, то... Это замыкание в C# через итераторы. Теперь код бота выглядит так:
Код: |
namespace StateScripting
{
public sealed class Test:Script
{
public Test ():base("ssTest", "ssTest", "State Scripting test")
{
}
protected override IEnumerable<ScriptOperation> Invoke ()
{
for (int i = 0; i<10; i++) {
Object target = Util.FindNextTarget();
yield return API.flyTo (target);
}
}
}
} |
Код выше просто последовательно летит к 10 выбранным целям.
Если разбираться в коде, то есть пару непоняток... Первое - это yield return - это просто перечислитель. Если вы сделаете
Код: |
foreach(ScriptOperation so in Test.Invkoe()){
Process(so);
} |
То вы увидите что Process вызовется 10 раз. Однако сахар тут не в этом... Давайте рассмотрим код API.flyTo():
Код: |
public static FlyTo flyTo(Object target){
return new FlyTo (target, DefaultDistance, UseBoosters);
} |
Фактически это "макрос"... "сахар" над созданием объекта ScriptOperation. ScriptOperation - эта та самая операция, которая растянута по времени. Прототип ScriptOperation довольно простой:
Код: |
namespace StateScripting.Operations
{
public class ScriptOperation
{
public enum FrameResult
{
Continue,
Done,
Error,
ErrorButContinue,
SimulateUI
}
public ScriptOperation ()
{
}
public virtual FrameResult Frame () {
return FrameResult.Done;
}
public virtual void Abort() {}
}
} |
У нас есть метод Frame, который и будет той самой машиной состояний, но выполняющий определенную команду, не более... В нашем случае - лететь к цели. Сам код flyTo я показывать не буду, ибо он не является целью этого поста/статьи... Однако надо показать как выполняется этот скрипт... Есть статический класс ScriptEngine, который содержит бибилиотеку скриптов, которые могут выполняться. У этого класса есть статический метод Frame:
Код: |
public static void Frame(){
foreach(Script s in m_scripts.Values){
if(s.Executing){
if (s.ResumeFrom != DateTime.MinValue) {
if (s.ResumeFrom <= DateTime.Now) {
s.ResumeFrom = DateTime.MinValue;
}
} else if (s.Operation != null) {
ScriptOperation.FrameResult result = ScriptOperation.FrameResult.Done;
try {
result = s.Operation.Frame ();
} catch (Exception e) {
Error (String.Format ("Error executing script {0}.\r\nStacktrace:\r\n{1}\r\nScript stopped.", s.Name, e));
s.Stop ();
}
switch (result) {
case ScriptOperation.FrameResult.Error:
Error (String.Format ("Script {0} returned error state and will be stopped"));
s.Stop ();
break;
case ScriptOperation.FrameResult.Done:
s.Operation = null;
break;
case ScriptOperation.FrameResult.ErrorButContinue:
break;
case ScriptOperation.FrameResult.SimulateUI:
s.ResumeFrom = DateTime.Now + TimeSpan.FromMilliseconds (100 + (uint)((new System.Random ()).NextDouble () * 100));
break;
}
} else {
if (s.ScriptState.MoveNext ()) {
s.Operation = s.ScriptState.Current;
} else {
s.Stop ();
}
}
}
}
} |
Нас тут интересует вот этот кусочек:
Код: |
if (s.ScriptState.MoveNext ()) {
s.Operation = s.ScriptState.Current;
} else {
s.Stop ();
}
|
Пазл начинает складываться... Фактически мы итерируемся по итератору, который является нашим скриптом бота и, как только ловим операцию (а не поймать ее мы не можем. Иначе скрипт закончен) - выполняем ее до тех пор, пока она не закончится, не вызывая код итератора.
Чтобы пазл совсем сложился надо показать код запуска скрипта:
Код: |
public virtual void Start(string[] args) {
ClearState();
Executing = true;
m_scriptState = Invoke ().GetEnumerator ();
} |
Вот теперь все понятно... Объект скрипта содержит в себе итератор, созданный из метода Invoke, по которому итерируется движок скриптов, получая следующую "временную" операцию, которую этот движок и выполняет. Временная итерация - это маленькая машина состояний. Таким образом полностью от машины состояний уйти не удалось, однако их стало больше и каждая из них стала гораздо проще.
Фактически, точно так-же работает система скриптинга в X'ах. Если вы все команды с @ замените на return yield, вы получите то-же самое.
Конечно, код не оптимален, его можно написать лучше и т.д. но основную идею, как можно использовать return yield в .Net я описал.
_________________ WARNING: By reading this post you accept that this post is genius.
Последний раз редактировалось: Варсик (17:33 08-07-2013), всего редактировалось 1 раз |
|
|
Shirson 1605 EGP
Рейтинг канала: 7(626) Репутация: 219 Сообщения: 16511 Откуда: 79°W 44°N Зарегистрирован: 29.01.2002 |
|
Варсик : |
Скажите, сколько народу умеют программировать конечные автоматы или машины состояний?
|
FSM это основа и фундамент программирования как такового. Не умея рабоать с и/или не понимая FSM, в профессиии делать нечего, вообще
_________________ У меня бисера не доxеpа. |
|
|
Варсик 545 EGP
Рейтинг канала: 4(81) Репутация: 117 Сообщения: 4041 Откуда: Москва Зарегистрирован: 22.12.2002 |
|
Ну... Это ты кодерам расскажи. Вообще там интуитивно понятно, но есть-же дибилы...
добавлено спустя 19 минут:
И тут-же появился вопрос...
А если надо получить результат долгой операции?
Вообще хочется что-то типа:
Код: |
uint res = yield return LongOp(); |
Но это невозможно. Можно сделать, конечно, так:
Код: |
unit res;
yield return LongOp(ref res); |
Но это как-то не красиво (( В то-же время сделать yield return внутри вызванной сабы - нельзя. Что печально.
_________________ WARNING: By reading this post you accept that this post is genius.
Последний раз редактировалось: Варсик (21:49 08-07-2013), всего редактировалось 1 раз |
|
|
Diff 708 EGP
Рейтинг канала: 2(11) Репутация: 44 Сообщения: 4179 Откуда: Сферическая Земля в вакууме. Зарегистрирован: 04.07.2003 |
|
Варсик : |
А если надо получить результат долгой операции?
|
Если б там действительно был FSM - то просто. FSM находится в состоянии Жду_Результата_Долгой_Операции и активируется по приходу сигнала Результат_Операции. Автомат, конечно, должен быть асинхронным.
Но у тебя тут описан не автомат, а какой-то странный мутант.
_________________ Конец света в конце тоннеля |
|
|
Minx 985 EGP
Рейтинг канала: 6(329) Репутация: 135 Сообщения: 10533 Откуда: Gomel, Belarus Зарегистрирован: 19.11.2005 |
|
Варсик : |
Самое простое, что можно было сделать: Запускать на каждый скрипт свой поток, который был-бы в постоянном синхроне. То есть... Когда выполняется операция со временем "лететь туда-то", то поток ждет выполнения этой операции, а машина состояний проверяет - закончилась-ли эта операция. Как только закончилась, поток получает управление, а основная программа ждет, пока поток разберется что и где ему надо сделать. Фактически принудительный свитч потока 2 раза с синхронизацией. При 60 FPS это ОЧЕНЬ дорогая реализация.
|
Reactor - берешь готовую реализацию и радуешься.
Реакторы бывают разные. Например Leader-Follower Model геморойна в реализации, но делает только один принудительный свитч.
---
По поводу мутанта согласен. Но я могу быть привередливым, т.к. в моем сознании автоматы больше ассоциируются с цветными сетями Петри, нежели с перебором списка действий.
_________________ μηδείς αγεωμέτρητος εισίτω |
|
|
Варсик 545 EGP
Рейтинг канала: 4(81) Репутация: 117 Сообщения: 4041 Откуда: Москва Зарегистрирован: 22.12.2002 |
|
Minx : |
Reactor - берешь готовую реализацию и радуешься.
|
И не находишь C#.
А реализовывать что-то страшное... Когда есть простой и удобный механизм как для платформы, так и для программиста... Я, конечно, мазахист, но не настолько.
_________________ WARNING: By reading this post you accept that this post is genius. |
|
|
Minx 985 EGP
Рейтинг канала: 6(329) Репутация: 135 Сообщения: 10533 Откуда: Gomel, Belarus Зарегистрирован: 19.11.2005 |
|
Википузией мир не ограничен.
Если все устраивает, то тут вообще вопросов нет (; Просто отписал, что то, что "можно было сделать", уже сделано до нас.
_________________ μηδείς αγεωμέτρητος εισίτω |
|
|
Варсик 545 EGP
Рейтинг канала: 4(81) Репутация: 117 Сообщения: 4041 Откуда: Москва Зарегистрирован: 22.12.2002 |
|
Почитал код... Мутно и не понятно... ИМХО они плохой пример взяли. В их варианте надо просто эвент луп крутить. А не этой фигней заниматься... epoll/kqueue с этим нормально справятся. Они для этого и сделаны. А в последнем epoll'е ты даже ветчить можешь в ядре и только потом отправлять сообщение в юзерспейс.
А так... Это смесь, причем не совсем удачная, моего метода и эвент лупа.
добавлено спустя 48 секунд:
Да и код там становится еще более непонятным, чем в случае с базовой стейт машиной. Я дрался за упрощение кода, а тут, походу, усложнение.
_________________ WARNING: By reading this post you accept that this post is genius.
Последний раз редактировалось: Варсик (01:20 09-07-2013), всего редактировалось 1 раз |
|
|
Sh.Tac. 151 EGP
Рейтинг канала: 5(108) Репутация: 14 Сообщения: 1426
Зарегистрирован: 27.07.2005 |
|
Варсик : |
А если надо получить результат долгой операции?
|
корутинам отказать, в шарпе 5.0 появился async await
_________________ This is what you get ...
(c) Radiohead |
|
|
Варсик 545 EGP
Рейтинг канала: 4(81) Репутация: 117 Сообщения: 4041 Откуда: Москва Зарегистрирован: 22.12.2002 |
|
Sh.Tac. : |
корутинам отказать, в шарпе 5.0 появился async await
|
К сожалению это не 5-й шарп. Это моно.
_________________ WARNING: By reading this post you accept that this post is genius. |
|
|
Olorin 70 EGP
Рейтинг канала: 1(6) Репутация: 12 Сообщения: 97 Откуда: Хьёрвард Зарегистрирован: 27.02.2006 |
|
Варсик : |
Sh.Tac. : |
корутинам отказать, в шарпе 5.0 появился async await
|
К сожалению это не 5-й шарп. Это моно.
|
В 2.11 же поддержка появилась вроде.. о_О
Да и смысл не столько в синтаксисе, а в концепции, кою некоторые даже на 2.0 перекладывали.. ээ.. Вот.
_________________ Мы на многое не отваживаемся не потому что оно трудно; оно трудно именно потому, что мы на него не отваживаемся.
Сенека Старший |
|
|
|
|
|
Канал Игры Мечты: «.Net скрываем конечный автомат или "бот v2.0"» |
|