3

Змейка на C

Это статья из тега Разработка игр от SAXAHOID
18828 27 14 октября 2016 г. Редакция
Недавно уже давно (а нечего посты задерживать!) мне захотелось написать небольшую и очень простенькую игру — змейку.
Для юниксового терминала. На чистом C — даже без ncurses и прочих подобных финтифлюшек.
А сегодня я расскажу и покажу, что получилось.

Недавноужедавно(анечегопостызадерживать!)мнезахотелосьнаписатьнебольшуюиоченьпростенькуюигру—змейку.Дляюниксовоготерминала.НачистомC—дажебезncursesипрочихподобныхфинтифлюшек.Асегодняярасскажуипокажу,чтополучилось.Тем,комунеинтересночитатьпост,аинтересносразуувидетькоди/илиисториюкоммитов,предлагаюпойтипоглядетьнарепозиторий.НебольшойдисклеймерДа,явкурсе,чтоможнодобавитьмножестворазныхфич(втомчислемультиплеериуровниспроцедурногенерируемымистенками)имногочегосделатьлучшекаквсамойигре,такивисходниках.Возможно,якогда-тоэтоисделаю.Наданныймоментничегоособопримечательноговкоденету,потомуиstupid-snakeНобылапоставленацельсделатьнемаксимальнохорошоикачественно,абыстроичтобыработало.Нуиещёповозможностинаглядно—идеянаписатьпоствсплылассамогоначала.Этацельвполноймередостигнута.НемногопоясненийC—довольнонизкоуровневыйязык,иужникакне"кроссплатформенныйпо-умолчанию",как,например,Python.Тотфакт,чтоянеиспользуюncurses,ещёбольшеусугубляетситуацию.Оченьмногоевигререализованопутёмфункций,имеющихсялишьвPOSIX-системах,атоивообщелишьвLinuxиglibc.WindowsPOSIX-системойнеявляется,даитерминалтамврядлинастолькожефункционален,потомусобратьstupid-snakeподWindowsувас,скореевсего,никакнеполучится.НаOSX—незнаю,пустькто-топопробуетидоложитобуспехах.МожеттакжеиненакаждомдистрибутивеLinuxсобраться,втакомслучаенапишитемне—помогуидобавлюнужныефлагивмейкфайл.Еслисвоеголинуксауваснигденезавалялось,аиспытатьигруоченьхочется,рекомендуюиспользоватьVirtualBoxилиинойсофтдляработысвиртуальнымимашинами,какойвыпредпочитаете.Еслижеваминтересенсампроцессиразъясненияккоду—высовершенноничегонетеряете,читаяэтотпостхотьсWindows,хотьсLinux,хотьсOS/2.Почемутерминал?Почемудажебезncurses?Зачемтакусложнятьсебежизнь?Во-первыхпотому,чтомнетакзахотелось.Во-вторыхпотому,чтоncurses—этооченьбольшая,толстаяисложнаялиба,такчтонефакт,чтобылобыпроще.Пояснять,скореевсего,сталобылишьсложнее.Япоймупост,еслинезнаюC?Понятиянеимею.ЕсливыдействительнонезнаетениC,нипреступноподобныхемуязыков(типаC++)—напишите,пожалуйста,поняливыилинет.Мнеинтересно.Постпланировалсякакпонятныйполным"чайникам",нополучилосьлиуменя—совсемдругойвопрос.Еслияпрочитаюпост,явыучуC?Нет.Еслияпрочитаюпост,янаучусьписатьигры?Нет.Скореевсего.Зачемтогдамнечитатьпост?Откудаязнаю,зачемвамчитатьпост?Может,ваминтересносмотреть,какдругиелюдимыслятирешаютзадачи.Может,ваминтересноузнать,какойэтоон—Cвконсолечкеюниксовой.Может,простоскучно.Впрочем,ядумаю,вмоёмпостеможнобудетотыскатьнекоторыеинтересныефактыиподходы,досихпорвамнеизвестные.Какъсобратьизапуститьвашеигрищебѣсовское,сударь?Длябояр,которыеэтогодосихпорникогданеделали,агуглитьлень,кратенькоепояснение:$gitclone'https://gitlab.com/saxahoid/stupid-snake.git'$cdstupid-snake$make$./snakeУправление?Стрелки,выход—Ctrl+Cиливрубитьсявстенку/себя.Авотявижуместо,гдекак-тооченьхитроподвывернутосделано,нельзялипроще?Скореевсего,нет.Какядальшепояснювпосте,впроцессеразработкивылезлонесколькооченьинтересныхособенностейработыстерминалом,иэтихитроподвывернутыекостыли—единственныйрабочийвариант.ПоехалиДавайте-касделаемигруЧтонужно,дабысделатьигрудлятерминала?Знатькакой-нибудьязыкпрограммированияболее-менееуверенноиуметьгуглить,чтобыразобратьсяснекоторымиособенностямиэтогосамоготерминала.Знатькучубиблиотек(нуразветолькоncurses,еслихочетсяболеесерьёзнуюигру,чемуменя,сминимумомгеморроя),особенностиработывашейлюбимойоконнойсистемы,даещёзаоднощедруюдозукомпьютернойграфикииужтемболеебытьдизайнером-художникомабсолютнонеобязательноидажевредно.Потомучтотакнеинтересно.Текстовыйввод/вывод—одинизсамыхпростыхиосновныхдлялюбогосовременногоЯП.Есливыуверенно(дадажеесликривосикосьнакось)знаетекакой-нибудь,товызнаетеикакработатьстекстом.Поэтомудлясозданияигры,использующейтекстдляобщенияспользователем,ненужнознатьничегоспецифичного.Поэтомужетакиеигры,дажееслипримитивныепосегодняшниммеркам,становятсянеплохимиупражнениямидлямозга.Всякаяразработка(нетолькоигр)проходитвнесколькоэтапов.Дляпростотыяисключуболеетворческиеисложнохарактеризуемыетипаидеи(онаужеесть)исосредоточусьнатехнических(дактомужеприменимыхвданномслучае;архитектуры,скажем,тутнету).ДизайнПрошузаметить,чтоимеетсяввидунетотдизайн,которымзанимаетсяАртемийЛебедев,атехническийдизайн.Оножепроектирование.Наэтапедизайнаструктурапрограммыобдумывается"набумажке",безнаписаниярабочегоисполняющегосякода(шаблоныделатьможно).Такойподходкажетсянепродуктивным,но,еслихорошоимовладеть,можноизбавитьсебяотмножества"Эврика!"-моментов,когдаполовинувсехисходниковприходитсяпереписыватьиз-заневерновыбранноговсамомначалерешения.Апотомещёразок.Затратынатакоерастутсразмерамипроектаоченьнехило,такчточастенькодаженесколькомесяцевчистогодизайнаокупаютсясполна.Видеалесадитьсязакодразработчикдолженлишьтогда,когдаужеполностьюпредставляетсебепрограмму.Комплекснаязадача(игра"Змейка")разбиваетсянаболеемелкие:•Обновлениеэкрана;Очевидно,длятого,чтобыиграбыладинамическойиотвечаланапользовательскийввод,нужнопериодическиобновлятьэкран.Таккак"змейка"—непоходоваязабава,делатьэтонеобходимопотаймеру.Нанашемламерскомуровнеболеечемдостаточновоспользоватьсякомандойвродеsleep(приказатьпрограмменичегонеделать1/fpsчастьсекунды,гдеfps—желаемаякадроваячастота),ноэтонеинтересноивообщеяболеюперфекционизмом,потомубудуиспользоватьчасыреальноговремени.•Визуализациязмейки;ТерминалсовременногоLinuxподдерживаетюникод—вотужгдепростордляфантазии!НопомоемумнениюреализацияюникодавсамомCнеточтобыочевидная,потому(дляначала,вовсякомслучае)решаюиспользоватьсамыйстандартныйизвсехстандартов—ASCII.Прямоетелозмеиизображатьбудусимволами"-|",повороты—"/\",направленнуювразныестороныголову(распахнутуюпасть)—"V<^>".Натомбыиостановиться,воттольконеинтересно(ияболеюперфекционизмом,помните?).Нужнаещёанимация.Какнасчётзаставлятьзмейкузакрыватьроткаждыйход?Досточнобудетзаменитьсимвол"головы"насоответствующийнаправлениюсимвол"тела".Переход"<"➛"-"сносносоздаётиллюзиюзакрывшегосярта.Такжестоит"выпрямлять"поворотытелазмеи,когдаонистановятсяпоследнимсегментом(небываетжеузмейтакихзагнутыххвостов).•Связьмеждуэлементамиигры;Змейкадолжнапогибнуть,наткнувшисьнастену,ивырасти,съевпищу.Длятого,чтобыувязатьмежсобойположениестен,еды,головы,телаихвостазмейки,необходимоиспользоватьобщуюдлянихвсехсистемукоординат.Исамыйочевидныйвариант—применитьстандартныекоординатытерминала(0,0—верхнийлевыйугол,иразрешение,разумеется,посимвольное—пропикселитерминалничегознатьнезнает).Дальшеостаётсявобщемдвапути:1)Создатьвсетребующиеучётаэлементыкакотдельныесущности,свойствамикоторыхявляютсякоординаты;2)Создатьоднусущность—координатноеполе—служащуюотображениемнынешнегосостояниятерминала,иразместитьэлементы-символывней.Первыйпуть—оченьгромоздкий.Например,дляучётаположенияедынеобходимобудетсоздатьмассив(атоисписок)сущностейтипа"еда",укаждойизкоторыхбудетсвойнаборкоординат.Чтобыпроверить,несъелализмейкаеду,нужнобудеткаждыйразпроходитьсяповсемуэтомумассиву/списку.Аналогичнопривыводенаэкранбудетнеобходимообойтикаждуюсущность-элемент,чтобыузнатьнужныекоординатыдлявывода.Второйпуть—намногоболеелегковесныйиудобный.Чтобыпроверить,ненаткнуласьлизмеяначто-то,достаточнопередеёперемещениемпроверить"новую"позицию—нетлитамужечего?Свыводомнаэкрантожелегко:простоprintкаждыйэлементизкоординатнойсетки.Ноестьипроблемаувторогопути—какнайтисамузмею?Обходитьвсёкоординатноеполепоиском?Какчастобывает,идеальныйвариант—комбинированный.Таккакинтересуетменявосновномзмея,авсёостальное—ужеотносительнонеё,дляудобстванахождениязмейкивкоординатномполеизобразимеёещёиотдельнойсущностью,хранящейвсебенынешниекоординаты.Ида,есливыподумали,чтопервыйвариант—безкоординатногополя—бесполезен,товысильноошибаетесь!Унегоестьсвоиприменения.Например,еслиполноеотображениекоординатногополяпотребуетдесяткагибибайтпамяти,тогдакаксущностейсоздатьнужнобылобывсегодесяток-другой.Ноподобныераскладынастолькожедалекиот"змейки",насколькоавторэтогопоста—отмировогогосподства.•Передвижениезмейки;Чтобыпередвигатьзмейку,необходимознатьнынешнююпозициюголовы,следующуюпозициюголовы,нынешнююпозициюхвостаиследующуюпозициюхвоста.Привыполнении"хода"нарисоватьголовунановойпозиции,сегменттела—настарой;старуюпозициюхвостаочистить.Новуюпозициюхвостанадознатьлишьдлятого,чтобызапомнитьеёнабудущее.Здесьвозможныразнообразныеварианты.Сучётомужеимеющейсявдизайнеотдельнойсущности"змея",хранящейкоординатыеёголовыдлябыстрогонахождения,мнепоказалосьсамымразумнымвэтужесущностьдобавитьтакжекоординатыхвоста.Дляопределенияновойпозицииголовыдостаточносохранитьеё(головы)направление(иинкрементировать/декрементироватькоординатынынешнейпозициивзависимостиотнаправления,чтобыполучитьновую).Аналогичнопоступитьсхвостом—хранитьегонаправление.Нотутвозникаетпроблема:еслиспеременойнаправленияголовывсёясно(игрокнажалвлево—повернутьголовувлево),токакбытьсхвостом?Нежелаяхранитьдополнительнуюинформациювбольшихобъёмахисключительнодляправильнойобработкихвоста,яприбегаюкпростомутрюку:знаянаправлениехвостаиследующийвэтомнаправлениисегменттела,новоенаправлениеопределитьоченьлегко.Например,еслихвостдвигалсяВПРАВО,иследующийсправасегмент—"-",хвостпродолжаетдвигатьсяВПРАВО.Еслинаблюдаетсясегмент"\",хвостповорачиваетВНИЗ.Если"/"—ВВЕРХ.Если"|",точто-тосломалось;такогослучитьсяникогданедолжно.•Генерацияеды;Генерируем,разумеется,случайнымобразом.Количествогенерируемойедызависитотразмеровполя;скажем,1единицанакаждые512пустыхпозиций.•Обработкастенок;Можнодержатьстенкивнутрикоординатногополя(ипроверятьстолкновениепопринципу"Являетсялиследующаяпозицияголовызанятасимволомстены?".Можнодержатьихснаружи(ипроверятьстолкновениепопринципу"Невышлалиголовазмеизаразрешённыепределыкоординат?").Мнебольшепонравилсявторойспособ.Онтакжекажетсяболеегибким(позволяетлегкозаменитьлогикустолкновениясостенойналогику"телепортации"надругойкрайполя—тожеинтересныйподход).Еслимненеизменяетпамять,именноперечисленныевышепунктыбылиобдуманымноюнаэтапедизайна.Выглядитсложно,нозанялоэтоуменявсего-топаручасовнесамогоусердногоразмышлениясблокнотом(ивсеготристраницыпоследнего).КодОткройтеисходник,ябудуссылатьсянаномерастрок(вквадратныхскобках),функцииитакдалее.Немногословофайловойструктурекода.Онаоченьпростая—одинисходник(snake.c),одинmakefile.LICENSEиREADME.mdнеиспользуютсяприсборкесовершенноникакиневажныдляпониманияпрограммы.По-хорошемуфункциинадобылобывынестивотдельныефайлыисгруппироватьтакимобразомпоихпредназначению,номнепоказалосьболееправильнымиспользоватьструктурупопроще.main()—строка515main—основнаяфункцияпрограммы;самапрограмма,еслипожелаете.Исполнениеначинаетсяотсюда.Таккакмненетнуждынаданныймоментприниматькакие-либоаргументыизкоманднойстроки,mainяобъявляюбезпараметров(void).Вначалевызываютсянесколькоинициализирующихфункций(переключениетерминалавнужныйрежим,установкаобработчикасигналаINT(Ctrl+C)иинициализациягенераторапсевдослучайныхчисел),затемобъявляютсянужныепеременные,затемпроизводитсяещёнесколькоинициализаций—ужекасающихсясамойигры—ипервыйвыводкартинкивтерминал.Наконец,получивзначениеначальногомоментавремени,кодвходитвциклобновленияизображения.Этотциклимеетнесколькоинтересныхособенностей.Начнёмсусловия—while(run).runявляется[64]переменнойособоготипаsig_atomic_t,придуманногоспециальнодляиспользованиявобработчикахсигналов.Свойкастомныйобработчик[82]яустанавливаюнастроке[517],ивсё,чтоонделает—сбрасываетпривызовеэтотсамыйrunв0.ОбрабатыватьSIGINTнужнозатем,чтобыкорректновыйтиизигрыивернутьтерминалвадекватноесостояние—стандартныйпредоставляемыйсистемойобработчикпростозавершаетпрограмму"здесьисейчас".Таккаксигналыдовольнопохожинапрерывания,ихобработчикитакжесхожи.Обработчиквызываетсявтотмомент,когдасигналприходит,внезависимостиотисполняемогоучасткапрограммы.Послезавершенияобработчика,исполнениевозвращаетсявстароеместоивсёпродолжается,какнивчёмнебывало.Икогдаяговорю"внезависимостиотисполняемогоучастка",яэтоиимеюввиду—обработчикможетбытьвызвандажесамизсебя.Ввидутакойспецификиприменениявобработчикахнельзяиспользоватьбольшуючастьфункций(насамомделе,существуетдажеофициальныйсписоктехстандартныхфункций,которыетакиможносуверенностьюприменятьвобработчиках.Оннебольшой).Хотя"нельзя"вCиозначаетвсегда"Можно,номывообщеничегонегарантируемитысамзавсёотвечаешь",янастоятельнонесоветуюэтопростоеправилонарушать.Следующийинтересныймомент—обновлениеэкрана.Немудрствуялукаво,яиспользуюздесьсамыйбанальныйподход:прямуюзависимостьскоростизмеиотчастотыкадров.Двакадра—одинходиоднаанимация.При10fpsполучаем5ходовзасекунду;при200fps—100.Работаетэтотак:прикаждойитерациицикласохраняетсямоментвремени[541],послечегорассчитываетсяразность[542]межнимимоментом,когдавпрошлыйразбылотрисованкадр.Затемполученнаяразностьсравниваетсяскадровойзадержкой(задержка=1секунда/fps),иеслионапревышаетзадержку—кадробновляется,сохранивновыймоментобновления.FPSнеявляетсячёткозаданнымконкретнымчислом,анаходитсякак"(уровень+1)*5".Инымисловами,напервомуровнеимеемчастоту10,навтором—15,натретьем—20,итакдалее.Кадровбываетдвавида:полноценныйианимационный.Определяюя,какойизнихнадорисовать,простымфлагомanimation_frame.Вслучаеанимационногокадраотрисовываетсяисключительноанимация;вслучаеполноценного—перерисовываетсявсёигровоеполе,атакжегенерируетсяпища,производитсяпонеобходимостилевел-апивсётакое.set_terminal_mode—строка90reset_input_mode—строка74Здесьпроисходиттерминальнаямагия.Впервойфункцииустанавливаетсянужныйрежим,вовторойвозвращаетсястарый.Вчёмдело?Дляработыспараметрамитерминалаиспользуетсянаборфункцийtermios.Происходитэтотак:вынутьнынешниепараметрывструктуруtermiosспомощьюфункцииtcgetattr[105],отредактироватьеё,запихнутьобратновтерминалспомощьюtcsetattr[109].По-умолчаниююниксовыйтерминалждётвводаполнойстроки(нажатияReturn)ипозволяетпередвводомэтустрокувсяческиредактировать.Этоназываетсяканоническимрежимом.Чтобынажатияпередавалисьнашейигресразу,безнеобходимостиклацатьEnter,необходимопереключитьтерминалврежимнеканонический.Такжепо-умолчаниююниксовыйтерминалсразувыводитлюбуюнажатуюклавишунаэкран.Этоназываетсяэхо-режимом.Чтобыэтогонепроисходило,надовыключитьэхо.Каноническийрежимиэхо-режимвыключаюодновременно[106].Этобитовыефлаги,иесливынепонимаетевообще,чтотампроисходит,тосоветуюпочитатьгде-нибудьопобитовыхоперациях—темаобъёмнаяиполезная.УстанавливаюVMIN[107]иVTIME[108]вмассивепараметровc_ccвнулевыезначения.VMINотвечаетзаминимальноеколичествосимволов,которогобудетдожидатьсятерминалвнеканоническомрежиме.Еслитамбудетчто-токроменуля,любаяпопыткачитатьстерминалазаблокируетпрограммудополученияVMINсимволов.Мнеэтосовершенноненужно,блокироваться—плохаяидея,надокадрыобновлять.VTIMEотвечаетзато,сколько(приненулевомVMIN)терминалбудетожидатьсимволы.Вообщеговоря,достаточноустановитьв0одинизэтихпараметров,ноиобасразу—неповредит.Ивset_terminal_mode,ивreset_terminal_modeимеютсяоченьстранныеprintf.Это—выводвтерминалособыхпоследовательностейбайтов,которыетерминал,следующийстандартуANSI,обязанпониматьиинтерпретировать.Каксимволыонизаписаныисключительнодляудобства.Полноеописаниеестьнавикипедии.Невдаваясьвподробности,"\x1b[?25l"означает"выключитьотображениекурсора",а"\x1b[?25h"—"включить".Интереснатакжефункцияatexit[113].Онарегистрируетлюбуюдругуюфункциювсистеметак,чтобытабылазапущенаприкорректномвыходеизпрограммы.Ключевоймоменттут—корректныйвыход;именночтобыобеспечитьегояперехватываюSIGINT.Наборфункцийturn_—строкисо121по219Этичетырефункцииоченьсхожимеждусобой.Ониустанавливаютновоенаправлениеголовызмеиирисуютновыйсегменттеланаместеголовы,еслиопределённыйповоротвозможенизнынешнейпозиции.Возвращаютint,работающийвкачествебулевойпеременной(можнобылобыобойтисьchar,номнепочему-тозахотелосьint).Рассмотримнапримереturn_right[196].Определяетсянынешнеенаправлениедвижения[201],ивзависимостиотнегоопределяютсядальнейшиедействия.Еслизмеядвигаласьвертикально(вверх[202]иливниз[205]),поворотвправовозможен—turn_rightвернёт1иустановитсоответствующийповоротусегменттеланаполе.Еслизмеяитакдвигаласьвправоиливообщеполучиланеправильныйаргумент[210],turn_rightпростовернёт1иничегобольшенесделает(этонужнодляэффектаускоренияпринажатойклавише).Еслизмеядвигаласьвлево[208],поворотнаправоневозможен—turn_rightвернёт0.Еслидержатьнажатойстрелкувкакую-тосторону,змейкадолжнадвигатьсявэтусторонурезвее.Яреализуюэтоповедениеоченьпростымспособом:экранобновляется(асоответственно,змейкадвижетсяраньше,чемейположенопотаймеру)прикаждомуспешномнажатииклавиши.Уэтогоподходаестьтолькоодинминус:ускорениезмейкизависитотпользовательскойнастройки,определяющейчастотуповторениясимволапризажатойклавише.Играпоменятьэтунастройкунеможет(даинедолжна).Номыработаемстерминалом,лучшегоспособапростонет.process_key—строка223Этафункциясчитываетстерминалатрибайта[227](именнотремябайтамикодируетсянажатиеклавиши-стрелки).Еслиэтитрибайтасоответствуютоднойиззаранееопределённыхконстант[55],вызываетсяфункциясоответствующегоповорота.Оченьважно,чтовданномслучаепеременнаяcинициализируетсяв0:такчетвёртыйбайт,которыйнесчитывается,новсёравноостаётся"болтаться",будетнулевыминестанетмешать.Вместопеременнойintможнобылоиспользоватьмассивchar,носравниватьстрёхбайтнымиконстантамиегонамногосложнее,чемпростымswitch.Возвращаетфункция1или0взависимостиоттого,смоглалисдвинутьсязмеявответнанажатиеклавиши.init_playfield—строка251Простенькаяфункцияинициализацииигровогополя.Простозабиваетеговсёпробелами.init_snake—строка259Функцияпосложнее,инициализациязмеи.Головазмеиустанавливаетсявусловныйцентрполяиизначальносмотритвправо.Ровновтужепозициюпомещаетсяхвост,длиназмеиустанавливаетсяв1ибуфердлины—в4.Такимобразом,начальнаядлиназмейки—5.Буфердлинынуженпотойпростойпричине,чтозаодинходзмеяспособнавырастировнонаодинсегмент,иникакнебольше.Тоесть(теоретический)бонус,сразудающий5очковдлины,возможнореализоватьтолькопутёмбуфера,гдеэта"ужесъеденная,ноещёнеобработанная"едабудетхранитьсяи"отращиваться"вхвостпооднойкаждыйход.Пронаблюдать,какименноэтоработает,можновфункцииmove_snake.redraw_all—строка278Функцияотрисовкиигровогополя.Вначалекурсорпомещаетсявстартовуюпозицию0,0(верхнийлевыйугол)спомощьюещёодногоуправляющегоспец-символа[279].Далеезанесколькоцикловвыводятсясначалаверхняястенка[281],затембоковыеисамополе[286],затемнижняястенка[295]инемногоинформации:длиназмеи,уровень[300].Ничегоинтересногоздесьнет;такоеписалкаждый,ктокогда-либовыводилматрицынаэкран.redraw_animation—строка305Немногоболееинтереснаяфункция,котораяотвечаетзаанимацию.Сиспользованием,опятьже,спец-символа(новэтотраздинамическисгенерированного)курсорустанавливаетсянакоординатыголовызмеи[307](поправки+2нужныиз-затого,чтовзмеехранятсякоординатыотносительноигровогополя-матрицы,анужныкоординатыотносительноэкрана).Далеевместоголовырисуется"закрытыйрот"(простосегменттела,соответствующийнаправлению)[309].Затемкурсорпутёмвсётойжеманипуляциисоспец-символомустанавливаетсянапозициюзмеиногохвоста[317]исхвостомпроисходитанимациявыпрямления(приусловии,чтоонещёнепрямой),вобщем-тоидентичнаятаковойдляголовы.move_snake—строка338Самаясложная,интереснаяиважнаяфункция.Перемещаетзмеювигровомполеивообщеобновляетеёсостояние.Еслинаправлениезмеинеменялось(поляdirиnew_dirодинаковы),значит,быланажатаклавишатогоженаправления,либонебыланажатаникакая.Вобеихэтихслучаяхфункцииturn_ничегонерисуют,потомунеобходимонарисоватьновыйсегменттела[345-350].Нерисуютturn_*именнопотому,чтовозможныхслучаятутдва(нажататаже,либоненажатаникакая),ивовторомслучаеturn_*вызванынебудут.Получается,дляэтоговторогослучаяотрисовкувнутриmove_snakeпредусматриватьвсёравнопридётся,аразонатутуженужна,зачемповторновturn_*?Еслиженаправлениепоменялось,new_dirсохраняетсякакdir[352].Далееопределяетсясимволдляотображенияголовыиобновляютсяеёкоординаты("головасдвинуласьнановоеместо").Этозависитисключительноотнаправленияголовыиобрабатываетсяпростымswitch[357].Затемноваяпозицияголовыанализируется.Еслитамнаданныймоментстена[379],еда[392]иличтоугодноещёкромепробела(этоможетбытьтолькотелозмеивовсехегоразнообразныхвариациях)[397],выполняютсясоответствующиедействия(gameoverилисъедениееды).Встречасостенойопределяетсякаквыходзарамкипозволенныхкоординат(этопозволяет,например,оченьлегконерисоватьстеныилизаменитьсимволстенынакакой-тоещё,дактомужепозволяетнезаниматьпамятьзазрясимволамистен,никогданеменяющимися).Наконец,послетогокаквсевозможныепроблемысновойпозициейулажены(иgameoverненаступил),тудапомещаетсяголовазмеи[407].Итеперьобрабатываетсяхвост[411].Еслиузмеинепустойбуфердлины,онадолжнавырасти;вэтомслучаесхвостомничегонепроисходитионостаётсянастаромместе[412].Еслижебуферпустой,всёнамногоинтереснее.Первымделомхвостзаменяетсяпробелом[415].Затемкоординатыхвостаобновляются,чтобыонзанялновуюпозицию:следующийпослестарогохвостасегменттела[418].Этотswitchоченьпохожнатаковойдляголовы[357],ноничегонерисуется.Дальнейшаязадача—определитьновоенаправлениехвоста.Сделатьэтоможно,проанализировавсегментнановойпозиции.Еслионпрямой,направлениехвостаостаётсяпрежним[437-439].Еслион"/",новоенаправлениеопределяетсявзависимостиотстарого[441].Еслион"\",направлениеопределяетсяаналогично,носдругимизначениями[359].Например,еслихвостдвигалсявверх,новыглядиткак"/",тодальшеемунужнодвигатьсявправо.gen_food—строка483Функциягенерациипищи.ОнауправляетсяконстантойFOOD_RARITY,определяющейчастотуедынаполекак"наFOOD_RARITYдоступногопространствадолженприходиться1юнитпищи".Соответственно,необходимоекол-воедыопределяетсяпростымделением[487].+1тамнужнодляокруглениявверх(тоестьчтобы,например,приустановленнойFOOD_RARITY512иразмереполя600,нанёмбылне1,а2кускаеды).Еслиедынаполенедостаточно(следитьзаеёколичествомможночерезпеременнуюfood_cnt),генерируетсяновыйкусок[490].Координатыедыопределяютсягенераторомслучайныхчиселиподгоняютсявзаданныекоординатныерамкипутёмоперациисуммыпомодулю(%),илижеостатка.Полученнаяпозицияпроверяется,иеслионапуста—тудапомещаетсяпища,аеслизанята—генерациявыполняетсявновь.Из-затого,чтогенерироватьсяедабудет"доупора",вслучае,близкомкпобеде(всёполезанятозмеёй),играможетначатьтормозить,акогдаместадлянеобходимогокол-ваедыпростонеостанется,банальноуйдётвбесконечныйцикл.Есливыкогда-либодобьётесьтакойситуации—выпобедили!Иудачисвыключениемигры,Ctrl+Cнесработает—онперехвачен.level_up—строка502Довольнопростаяфункция,повышающаяигровойуровень(азначит—скорость)подостижениюзмеиочереднойLVLUP_LENGTH.Например,вмоёмслучаеLVLUP_LENGTHустановленав50—таклевелапбудетпроисходитькаждые50очковдлины.Исползуетсяstatic(сохраняющаясвоёзначениемежвызовамифункции)переменнаяupped_already,чтобыизбежатьпроблемпринесколькихвызовахlevel_upзатехода,покадлиназмеисохраняетсякратнойLVLUP_LENGTH.Онаустанавливаетсяв1послеповышенияуровня,ив0,когдадлиназмеинесоответствуетнужномудлялевел-апазначению.Таккакдлинаможетлишьрасти,этоадекватныйподход.ПослесловиеНадеюсь,кому-тобылоинтереснопрочитатьпрото,какможноупоротоихардкорнонаписатьзмейку.Ядлясебявыношуоднуистину:писатьпостыпросвойкодоченьсложно.Мнебылофизическитяжеловозвращатьсякэтомупостуипытатьсявыразитьвсловахто,чтокажетсяочевиднымизструктурыпрограммы.Возможно,вдругойразяпопробуюкакой-нибудьинойформат.https://youtu.be/4VNCKf_hzZ8

Комментарии редакции и автора поста

Тут шеф-редактор сообщает о найденых ошибках, дает рекомендации. Автор поста, в свою очередь, тоже может что-то сказать, спросить или уточнить.


Комментариев пока нет.


Добавлять комменты могут только редакторы и автор поста.