Привет, я Семён
Unity-разработчик
Пишу чистый C# код. Создаю архитектуру игр и приложений на Unity.
Обо мне
Кто я и чем занимаюсь
Я Unity-разработчик с фокусом на программирование и архитектуру кода. Специализируюсь на C#, проектировании систем, оптимизации и написании чистого, поддерживаемого кода. Работаю над играми и интерактивными приложениями, где основная ценность — качественный код и продуманная архитектура.
Навыки
Технологии и инструменты, с которыми я работаю
Unity & C#
Web
Инструменты
SDK & Платформы
Дизайн
Проекты
Проекты, в которых я принимал участие
«Очень странная удача»
Интерактивная игра. Реализована на Unity с интеграцией YandexSDK.
MapPinner
Веб-приложение для создания заметок на карте. Совместное редактирование в реальном времени.
Проекты для Яндекс Игр
Опыт работы с платформой Яндекс Игр, 5+ опубликованных проектов на Unity.
Преподавание в Академии TOP
Преподаю в «Академия TOP»: Unity и C#, а также Blender, SketchUp, Tinkercad, Python и другие IT-дисциплины.
Видео с геймплеем
Короткие демонстрации моих игровых проектов
Toy Air Dogs
QR-Clover (MyIndie January Rush Lvl 8)
QR-FishArena
Архитектура кода и примеры
Реальные примеры кода из проекта MapPinner. Кликните по блоку, чтобы открыть соответствующий код.
using Application;
using Domain;
using Infrastructure;
using Input;
using Presentation;
using UnityEngine;
namespace Bootstrap
{
// Composition Root — сборка зависимостей проекта
public class GameInstaller : MonoBehaviour
{
[Header("Scene References")]
[SerializeField] private MapCameraView _cameraView;
[SerializeField] private MapView _mapView;
[SerializeField] private Transform _pinsContainer;
[Header("UI Panels")]
[SerializeField] private PinPreviewPanel _previewPanel;
[SerializeField] private PinDetailPanel _detailPanel;
[SerializeField] private PinEditPanel _editPanel;
[Header("Prefabs")]
[SerializeField] private PinView _pinPrefab;
private MapState _mapState;
private CameraNavigationSystem _cameraNavSystem;
private PinInteractionSystem _pinInteractionSystem;
private MapPersistenceService _persistenceService;
private PinCreationService _pinCreationService;
private PinViewFactory _pinViewFactory;
void Start()
{
var mouseInput = new UnityMouseInput();
var mapRepository = new JsonMapRepository();
var selectionService = new PinSelectionService();
_persistenceService = new MapPersistenceService(mapRepository);
_pinCreationService = new PinCreationService();
_cameraNavSystem = new CameraNavigationSystem(
mouseInput, _cameraView, _mapView);
_pinInteractionSystem = new PinInteractionSystem(
mouseInput, selectionService, _cameraView);
_pinViewFactory = new PinViewFactory(_pinPrefab, _pinsContainer);
_mapState = _persistenceService.Load(new MapId("default"));
foreach (var pin in _mapState.Pins)
CreatePinView(pin);
}
void Update()
{
_cameraNavSystem.Tick();
_pinInteractionSystem.Tick(_mapState);
}
void OnApplicationQuit()
{
_persistenceService.Save(_mapState);
}
}
}
using Domain;
using Input;
using Presentation;
using UnityEngine;
namespace Application
{
// Use Case — обрабатывает взаимодействие пользователя с пинами
public class PinInteractionSystem
{
private enum State { Idle, Pressed, Dragging }
private State _currentState = State.Idle;
private readonly IMouseInput _input;
private readonly PinSelectionService _selectionService;
private readonly MapCameraView _cameraView;
private Vector2 _pressStartPosition;
private float _pressTime;
private PinEntity _pressedPin;
public System.Action OnEmptySpaceClick;
public System.Action OnPinSelected;
public System.Action OnPinDragStart;
public System.Action OnPinDragEnd;
public void Tick(MapState mapState)
{
var mousePos = _input.GetMousePosition();
var pinUnderMouse = _selectionService.FindPinAtScreenPosition(
new Vector2(mousePos.x, mousePos.y),
mapState.Pins,
wp => _cameraView.WorldToScreen(wp),
HitTolerance
);
switch (_currentState)
{
case State.Idle:
if (_input.GetLeftMouseButtonDown())
{
_currentState = State.Pressed;
_pressedPin = pinUnderMouse;
_pressStartPosition = _input.GetMousePosition();
}
break;
case State.Pressed:
_pressTime += Time.deltaTime;
if (!_input.IsLeftMouseButtonPressed())
{
float moved = Vector2.Distance(
_pressStartPosition, _input.GetMousePosition());
if (moved < ClickMaxDistance)
{
if (_pressedPin != null)
OnPinSelected?.Invoke(_pressedPin);
else
OnEmptySpaceClick?.Invoke(
_cameraView.ScreenToWorld(mousePos));
}
_currentState = State.Idle;
}
else if (_pressTime > HoldThreshold && _pressedPin != null)
{
_currentState = State.Dragging;
OnPinDragStart?.Invoke(_pressedPin);
}
break;
case State.Dragging:
if (_input.GetLeftMouseButtonUp())
{
var worldPos = _cameraView.ScreenToWorld(mousePos);
OnPinDragEnd?.Invoke(_pressedPin, worldPos);
_currentState = State.Idle;
}
break;
}
}
}
}
using Domain;
using UnityEngine;
namespace Application
{
// Сервис создания новых пинов
public class PinCreationService
{
public PinEntity CreatePin(Vector2 worldPosition, string title = "")
{
var pin = new PinEntity(
PinId.NewId(),
worldPosition,
title: string.IsNullOrEmpty(title) ? "Новая заметка" : title
);
return pin;
}
public PinEntity CreatePinFromClipboard(Vector2 worldPosition)
{
var imagePath = TrySaveClipboardImage();
var pin = new PinEntity(
PinId.NewId(),
worldPosition,
title: "Из буфера обмена",
imagePath: imagePath
);
return pin;
}
private string TrySaveClipboardImage()
{
// Сохранение изображения из буфера обмена
return string.Empty;
}
}
}
using System.Collections.Generic;
using System.Linq;
using Domain;
using UnityEngine;
namespace Application
{
// Сервис определения пина под курсором
public class PinSelectionService
{
private const float DefaultHitRadius = 30f;
public PinEntity FindPinAtScreenPosition(
Vector2 screenPos,
IEnumerable pins,
System.Func worldToScreen,
float hitRadius = DefaultHitRadius)
{
return pins
.Select(pin => new {
Pin = pin,
ScreenPos = worldToScreen(pin.Position),
Distance = Vector2.Distance(screenPos, worldToScreen(pin.Position))
})
.Where(x => x.Distance < hitRadius)
.OrderBy(x => x.Distance)
.Select(x => x.Pin)
.FirstOrDefault();
}
}
}
using Domain;
using Infrastructure;
namespace Application
{
// Сервис сохранения и загрузки карты
public class MapPersistenceService
{
private readonly IMapRepository _repository;
public MapPersistenceService(IMapRepository repository)
{
_repository = repository;
}
public MapState Load(MapId mapId)
{
var data = _repository.Load(mapId.Value);
return new MapState(data);
}
public void Save(MapState mapState)
{
var data = mapState.ToData();
_repository.Save(data);
}
public bool HasSave(MapId mapId)
{
return _repository.Load(mapId.Value) != null;
}
}
}
using System;
using UnityEngine;
namespace Domain
{
// Entity — основная доменная сущность
public class PinEntity
{
public PinId Id { get; }
public Vector2 Position { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string ImagePath { get; set; }
public string AudioPath { get; set; }
public PinEntity(PinId id, Vector2 position,
string title = "", string description = "",
string imagePath = "", string audioPath = "")
{
Id = id;
Position = position;
Title = title;
Description = description;
ImagePath = imagePath;
AudioPath = audioPath;
}
}
}
using System;
namespace Domain
{
// Value Object — неизменяемый идентификатор пина
public readonly struct PinId : IEquatable
{
public Guid Value { get; }
public PinId(Guid value) => Value = value;
public static PinId NewId() => new(Guid.NewGuid());
public bool Equals(PinId other) => Value.Equals(other.Value);
public override bool Equals(object obj) => obj is PinId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
}
}
using System.Collections.Generic;
namespace Domain
{
// Aggregate — управляет состоянием карты и пинов
public class MapState
{
private readonly Dictionary _pins = new();
public readonly MapId MapId;
public IEnumerable Pins => _pins.Values;
public MapState(MapData data)
{
MapId = data.MapId;
foreach (var pin in data.Pins)
_pins[pin.Id] = pin;
}
public void AddPin(PinEntity pin) => _pins[pin.Id] = pin;
public PinEntity GetPin(PinId id) =>
_pins.TryGetValue(id, out var pin) ? pin : null;
public void RemovePin(PinId id) => _pins.Remove(id);
public MapData ToData()
{
return new MapData
{
MapId = this.MapId,
Pins = new List(_pins.Values)
};
}
}
}
using System.Collections.Generic;
using System;
namespace Domain
{
// DTO — данные карты для сериализации
[Serializable]
public class MapData
{
public MapId MapId;
public List Pins = new();
public MapData() { }
public MapData(MapId mapId, List pins)
{
MapId = mapId;
Pins = pins;
}
}
}
using System;
namespace Domain
{
// Value Object — идентификатор карты
public readonly struct MapId : IEquatable
{
public string Value { get; }
public MapId(string value) => Value = value;
public bool Equals(MapId other) => Value == other.Value;
public override bool Equals(object obj) => obj is MapId other && Equals(other);
public override int GetHashCode() => Value?.GetHashCode() ?? 0;
public override string ToString() => Value ?? "unknown";
}
}
namespace Infrastructure
{
// Репозиторий — паттерн для доступа к данным
public interface IMapRepository
{
MapData Load(string mapId);
void Save(MapData data);
}
}
using System.IO;
using UnityEngine;
namespace Infrastructure
{
// Реализация репозитория через JSON-файлы
public class JsonMapRepository : IMapRepository
{
private readonly string _savePath;
public JsonMapRepository()
{
_savePath = Application.persistentDataPath;
}
public MapData Load(string mapId)
{
var path = Path.Combine(_savePath, $"{mapId}.json");
if (!File.Exists(path))
{
Debug.Log($"[JsonMapRepository] No save found: {path}");
return new MapData();
}
var json = File.ReadAllText(path);
return JsonUtility.FromJson(json);
}
public void Save(MapData data)
{
var path = Path.Combine(_savePath, $"{data.MapId}.json");
var json = JsonUtility.ToJson(data, prettyPrint: true);
File.WriteAllText(path, json);
Debug.Log($"[JsonMapRepository] Saved map: {data.MapId}");
}
}
}
using UnityEngine;
namespace Presentation
{
// Основной View карты
public class MapView : MonoBehaviour
{
[SerializeField] private RectTransform _mapContainer;
public Vector2 GetMapSize()
{
return _mapContainer.rect.size;
}
public Vector2 ScreenToMapPosition(Vector2 screenPos)
{
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_mapContainer, screenPos, null, out var localPoint);
return localPoint;
}
}
}
using UnityEngine;
namespace Presentation
{
// View камеры — управление положением и зумом
public class MapCameraView : MonoBehaviour
{
[SerializeField] private new Camera camera;
[SerializeField] private float defaultZoom = 3f;
public float Zoom { get; private set; }
private Vector3 _targetPosition;
private float _targetZoom;
void Start()
{
Zoom = defaultZoom;
_targetZoom = defaultZoom;
_targetPosition = transform.position;
}
public void Move(Vector2 delta)
{
_targetPosition += new Vector3(delta.x, delta.y, 0);
}
public void SetZoom(float zoom)
{
_targetZoom = zoom;
}
public Vector2 WorldToScreen(Vector2 worldPos)
{
return camera.WorldToScreenPoint(
new Vector3(worldPos.x, worldPos.y, 0));
}
public Vector2 ScreenToWorld(Vector2 screenPos)
{
var world = camera.ScreenToWorldPoint(
new Vector3(screenPos.x, screenPos.y, 0));
return new Vector2(world.x, world.y);
}
void LateUpdate()
{
transform.position = Vector3.Lerp(
transform.position, _targetPosition, Time.deltaTime * 10f);
Zoom = Mathf.Lerp(Zoom, _targetZoom, Time.deltaTime * 10f);
camera.orthographicSize = Zoom;
}
}
}
using Domain;
using UnityEngine;
using UnityEngine.UI;
namespace Presentation
{
// View-компонент пина на карте
public class PinView : MonoBehaviour
{
[SerializeField] private Image _icon;
[SerializeField] private GameObject _selectionIndicator;
[SerializeField] private CanvasGroup _canvasGroup;
public PinEntity BoundPin { get; private set; }
public void Bind(PinEntity pin)
{
BoundPin = pin;
UpdatePosition();
}
public void UpdatePosition()
{
var screenPos = Camera.main.WorldToScreenPoint(
new Vector3(BoundPin.Position.x, BoundPin.Position.y, 0));
transform.position = screenPos;
}
public void SetSelected(bool selected)
{
_selectionIndicator.SetActive(selected);
_canvasGroup.alpha = selected ? 1f : 0.7f;
}
public void SetHighlight(bool highlight)
{
transform.localScale = highlight ? Vector3.one * 1.15f : Vector3.one;
}
}
}
using Domain;
using UnityEngine;
namespace Presentation
{
// Фабрика для создания View пинов
public class PinViewFactory
{
private readonly PinView _prefab;
private readonly Transform _container;
public PinViewFactory(PinView prefab, Transform container)
{
_prefab = prefab;
_container = container;
}
public PinView Create(PinEntity pin)
{
var view = Object.Instantiate(_prefab, _container);
view.Bind(pin);
return view;
}
public void Destroy(PinView view)
{
Object.Destroy(view.gameObject);
}
}
}
using Domain;
using UnityEngine;
using UnityEngine.UI;
namespace Presentation
{
// Панель детального просмотра пина
public class PinDetailPanel : MonoBehaviour
{
[SerializeField] private GameObject _panel;
[SerializeField] private Text _titleText;
[SerializeField] private Text _descriptionText;
[SerializeField] private Image _previewImage;
private PinEntity _currentPin;
public void Show(PinEntity pin)
{
_currentPin = pin;
_titleText.text = pin.Title;
_descriptionText.text = pin.Description;
_panel.SetActive(true);
}
public void Hide()
{
_panel.SetActive(false);
_currentPin = null;
}
}
}
using Domain;
using UnityEngine;
using UnityEngine.UI;
namespace Presentation
{
// Панель предпросмотра пина (мини)
public class PinPreviewPanel : MonoBehaviour
{
[SerializeField] private GameObject _panel;
[SerializeField] private Text _titleText;
[SerializeField] private Image _previewImage;
[SerializeField] private float _followOffset = 20f;
private PinEntity _currentPin;
public void ShowAt(PinEntity pin, Vector2 screenPos)
{
_currentPin = pin;
_titleText.text = pin.Title;
_panel.transform.position = screenPos + Vector2.up * _followOffset;
_panel.SetActive(true);
}
public void Hide()
{
_panel.SetActive(false);
_currentPin = null;
}
void Update()
{
if (_currentPin != null)
{
var screenPos = Camera.main.WorldToScreenPoint(
new Vector3(_currentPin.Position.x, _currentPin.Position.y, 0));
_panel.transform.position = screenPos + Vector3.up * _followOffset;
}
}
}
}
using Domain;
using UnityEngine;
using UnityEngine.UI;
namespace Presentation
{
// Панель редактирования пина
public class PinEditPanel : MonoBehaviour
{
[SerializeField] private GameObject _panel;
[SerializeField] private InputField _titleInput;
[SerializeField] private InputField _descriptionInput;
[SerializeField] private Button _saveButton;
[SerializeField] private Button _deleteButton;
[SerializeField] private Button _cancelButton;
private PinEntity _editingPin;
public void Open(PinEntity pin)
{
_editingPin = pin;
_titleInput.text = pin.Title;
_descriptionInput.text = pin.Description;
_panel.SetActive(true);
}
public void Close()
{
_panel.SetActive(false);
_editingPin = null;
}
public void Save()
{
if (_editingPin == null) return;
_editingPin.Title = _titleInput.text;
_editingPin.Description = _descriptionInput.text;
Close();
}
public void Delete()
{
if (_editingPin == null) return;
// Сигнал для удаления
Close();
}
}
}
using UnityEngine;
namespace Input
{
// Абстракция ввода — позволяет тестировать без реального мыши
public interface IMouseInput
{
Vector2 GetMousePosition();
bool GetLeftMouseButtonDown();
bool IsLeftMouseButtonPressed();
bool GetLeftMouseButtonUp();
bool GetMouseButtonDown(int button);
bool GetMouseButtonUp(int button);
float GetMouseScrollDelta();
}
}
using UnityEngine;
namespace Input
{
// Реализация через Unity Input System
public class UnityMouseInput : IMouseInput
{
public Vector2 GetMousePosition()
=> UnityEngine.Input.mousePosition;
public bool GetLeftMouseButtonDown()
=> UnityEngine.Input.GetMouseButtonDown(0);
public bool IsLeftMouseButtonPressed()
=> UnityEngine.Input.GetMouseButton(0);
public bool GetLeftMouseButtonUp()
=> UnityEngine.Input.GetMouseButtonUp(0);
public bool GetMouseButtonDown(int button)
=> UnityEngine.Input.GetMouseButtonDown(button);
public bool GetMouseButtonUp(int button)
=> UnityEngine.Input.GetMouseButtonUp(button);
public float GetMouseScrollDelta()
=> UnityEngine.Input.mouseScrollDelta.y;
}
}
Опыт работы
Места, где я работал и набирался опыта
Unity-разработчик — Малые игровые студии
2022 — 2025Разработка игр на Unity для мобильных платформ, веба и ПК. Оптимизация производительности, работа с Asset Bundles/Addressables, интеграция SDK.
Unity-разработчик — Яндекс Игры (фриланс)
2024 — 2025Веб
Unity-разработчик — QR-Games.ru
2025 — настоящее времяПати-игры для компании. Реализация мультиплеера.
Преподаватель IT — Академия TOP
Март 2026 — настоящее времяПреподавание курса по Unity-разработке. Ведение дипломных проектов, подготовка методических материалов.
Контакты
Свяжитесь со мной — буду рад новым знакомствам и предложениям