Привет, я Семён

Unity-разработчик

Пишу чистый C# код. Создаю архитектуру игр и приложений на Unity.

Обо мне

Кто я и чем занимаюсь

Семён Малышев

Я Unity-разработчик с фокусом на программирование и архитектуру кода. Специализируюсь на C#, проектировании систем, оптимизации и написании чистого, поддерживаемого кода. Работаю над играми и интерактивными приложениями, где основная ценность — качественный код и продуманная архитектура.

2+ Года коммерческой разработки
20+ Опубликованных проектов
🎓 Имеется опыт преподавания

Навыки

Технологии и инструменты, с которыми я работаю

🎮

Unity & C#

C# (LINQ, async/await, DI) Unity Engine (2020.3+) Unity UI / uGUI Addressables / Asset Bundles DOTween / PrimeTween ECS (Entities) WebGL-сборки
🌐

Web

JavaScript (базовый) HTML5 / CSS3 REST API WebSocket
🛠

Инструменты

Git / GitHub / Git Flow Rider / VS Code Jira / Trello / YouTrack CI/CD (Github Actions)
📦

SDK & Платформы

YandexSDK (Yandex Games) MirraSDK Playgamma SDK WebGL / Mobile / PC
🎨

Дизайн

Figma (базовый) Photoshop Blender Aseprite

Проекты

Проекты, в которых я принимал участие

🎮
Web

«Очень странная удача»

Интерактивная игра. Реализована на Unity с интеграцией YandexSDK.

Unity YandexSDK WebGL
Смотреть проект
🎲
Web, Mobile

QR-Games

Пати-игры для компании. Реализация мультиплеера.

Unity Photon
Смотреть проект
🗺️
Web

MapPinner

Веб-приложение для создания заметок на карте. Совместное редактирование в реальном времени.

JavaScript Firebase
Исходный код
🏆
Web (Яндекс Игры)

Проекты для Яндекс Игр

Опыт работы с платформой Яндекс Игр, 5+ опубликованных проектов на Unity.

Unity YandexSDK WebGL
📚
Офлайн

Преподавание в Академии TOP

Преподаю в «Академия TOP»: Unity и C#, а также Blender, SketchUp, Tinkercad, Python и другие IT-дисциплины.

C# Unity Методические материалы

Видео с геймплеем

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

Toy Air Dogs

QR-Clover (MyIndie January Rush Lvl 8)

QR-FishArena

Архитектура кода и примеры

Реальные примеры кода из проекта MapPinner. Кликните по блоку, чтобы открыть соответствующий код.

Bootstrap
GameInstaller
Application
PinInteractionSystem
PinCreationService
PinSelectionService
MapPersistenceService
CameraNavigationSystem
Domain
PinEntity
PinId
MapState
MapData
MapId
Infrastructure
IMapRepository
JsonMapRepository
Presentation
MapView
MapCameraView
PinView
PinViewFactory
PinDetailPanel
PinPreviewPanel
PinEditPanel
Input
IMouseInput
UnityMouseInput
GameInstaller.cs
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);
        }
    }
}
PinInteractionSystem.cs
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;
            }
        }
    }
}
PinCreationService.cs
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;
        }
    }
}
PinSelectionService.cs
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();
        }
    }
}
MapPersistenceService.cs
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;
        }
    }
}
CameraNavigationSystem.cs
using Input;
using Presentation;
using UnityEngine;

namespace Application
{
    // Система управления камерой: панорама, зум
    public class CameraNavigationSystem
    {
        private readonly IMouseInput _input;
        private readonly MapCameraView _cameraView;
        private readonly MapView _mapView;

        private Vector2 _lastMousePos;
        private bool _isDragging;

        private const float ZoomSpeed = 0.1f;
        private const float DragSpeed = 1f;
        private const float MinZoom = 0.5f;
        private const float MaxZoom = 5f;

        public CameraNavigationSystem(
            IMouseInput input,
            MapCameraView cameraView,
            MapView mapView)
        {
            _input = input;
            _cameraView = cameraView;
            _mapView = mapView;
        }

        public void Tick()
        {
            HandleZoom();
            HandlePan();
        }

        private void HandleZoom()
        {
            float scroll = _input.GetMouseScrollDelta();
            if (Mathf.Abs(scroll) > 0.01f)
            {
                float newZoom = _cameraView.Zoom - scroll * ZoomSpeed;
                newZoom = Mathf.Clamp(newZoom, MinZoom, MaxZoom);
                _cameraView.SetZoom(newZoom);
            }
        }

        private void HandlePan()
        {
            if (_input.GetMouseButtonDown(2) || _input.GetMouseButtonDown(1))
            {
                _isDragging = true;
                _lastMousePos = _input.GetMousePosition();
            }

            if (_input.GetMouseButtonUp(2) || _input.GetMouseButtonUp(1))
                _isDragging = false;

            if (_isDragging)
            {
                Vector2 delta = _input.GetMousePosition() - _lastMousePos;
                _cameraView.Move(delta * DragSpeed * -1f);
                _lastMousePos = _input.GetMousePosition();
            }
        }
    }
}
PinEntity.cs
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;
        }
    }
}
PinId.cs
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();
    }
}
MapState.cs
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)
            };
        }
    }
}
MapData.cs
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;
        }
    }
}
MapId.cs
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";
    }
}
IMapRepository.cs
namespace Infrastructure
{
    // Репозиторий — паттерн для доступа к данным
    public interface IMapRepository
    {
        MapData Load(string mapId);
        void Save(MapData data);
    }
}
JsonMapRepository.cs
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}");
        }
    }
}
MapView.cs
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;
        }
    }
}
MapCameraView.cs
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;
        }
    }
}
PinView.cs
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;
        }
    }
}
PinViewFactory.cs
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);
        }
    }
}
PinDetailPanel.cs
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;
        }
    }
}
PinPreviewPanel.cs
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;
            }
        }
    }
}
PinEditPanel.cs
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();
        }
    }
}
IMouseInput.cs
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();
    }
}
UnityMouseInput.cs
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 C# WebGL SDK

Unity-разработчик — Яндекс Игры (фриланс)

2024 — 2025

Веб

YandexSDK WebGL Unity

Unity-разработчик — QR-Games.ru

2025 — настоящее время

Пати-игры для компании. Реализация мультиплеера.

Unity Photon

Преподаватель IT — Академия TOP

Март 2026 — настоящее время

Преподавание курса по Unity-разработке. Ведение дипломных проектов, подготовка методических материалов.

C# Unity Teaching

Контакты

Свяжитесь со мной — буду рад новым знакомствам и предложениям