Popcorning Guinea Pig
Search Results for

    Card System

    The Popcorning Guinea Pig engine includes a comprehensive 3D card system designed for building card games, deck builders, and other card-based interactions. The system provides Card3D components for individual cards and CardPile3D for managing collections of cards, with full support for 3D positioning, animations, and interactive sections.

    Overview

    The card system offers:

    • 3D Card Rendering: Full 3D card models with front and back textures
    • Interactive Sections: Define clickable regions on cards with tooltips
    • Card Piles: Manage collections of cards with shuffling and drawing
    • Nine-Slice Support: Scalable card borders and decorations
    • Drag & Drop: Built-in support for card manipulation
    • Visual Effects: Highlights, animations, and particle effects
    • PGPSCN Integration: Define cards declaratively in scene files

    Core Components

    Card3D

    The Card3D class represents individual cards in 3D space:

    using PopcorningGuineaPig.Engine.UI;
    
    // Create a 3D card
    var card = new Card3D(
        frontTexture,           // Front face texture
        backTexture,            // Back face texture  
        position,               // World position
        new Vector2(1.5f, 2.0f), // Card dimensions
        0.05f                   // Card thickness
    );
    
    // Configure the card
    card.SetFaceUp(true);
    card.SetPosition(new Vector3(0, 0.5f, 0));
    card.SetRotation(new Vector3(0, 45, 0));
    

    CardPile3D

    The CardPile3D class manages collections of cards:

    using PopcorningGuineaPig.Engine.UI;
    
    // Create a card pile
    var drawPile = new CardPile3D(
        new Vector3(-4, 0.2f, -2.0f), // Position
        Vector3.Zero,                 // Rotation
        0.01f                        // Stack offset per card
    );
    
    // Add cards to the pile
    drawPile.AddCard(card, animate: true);
    
    // Draw cards from the pile
    Card3D? drawnCard = drawPile.DrawCard(animate: true);
    
    // Shuffle the pile
    drawPile.Shuffle();
    

    Card Data Structure

    Define card properties using the CardData class:

    public enum CardSuit
    {
        Hearts,
        Diamonds, 
        Clubs,
        Spades
    }
    
    public enum CardValue
    {
        Ace = 1,
        Two = 2,
        // ... through King = 13
    }
    
    public class CardData
    {
        public CardSuit Suit { get; }
        public CardValue Value { get; }
        
        public CardData(CardSuit suit, CardValue value)
        {
            Suit = suit;
            Value = value;
        }
        
        public override string ToString()
        {
            return $"{Value} of {Suit}";
        }
    }
    

    Interactive Sections

    Cards can have defined interactive regions with tooltips and click handling:

    // Add interactive sections to a card
    card.AddSection("title", new Rectangle(0.1f, 0.05f, 0.8f, 0.15f));     // Top section
    card.AddSection("artwork", new Rectangle(0.15f, 0.25f, 0.7f, 0.4f));   // Center section  
    card.AddSection("loot", new Rectangle(0.1f, 0.75f, 0.8f, 0.2f));       // Bottom section
    
    // Check if mouse is over a section
    Vector2 mousePos = Raylib.GetMousePosition();
    string sectionName;
    if (card.IsPointOver(mousePos, camera, out sectionName))
    {
        // Handle section hover/click
        switch (sectionName)
        {
            case "title":
                ShowTooltip("Card Title - Shows the name of this card");
                break;
            case "artwork":
                ShowTooltip("Card Artwork - The main illustration");
                break;
            case "loot":
                ShowTooltip("Loot Icons - Rewards for playing this card");
                break;
        }
    }
    

    Nine-Slice Support

    Cards support nine-slice textures for scalable borders:

    // Set nine-slice borders (10 pixel corners)
    card.SetFrontNineSlice(10);
    card.SetBackNineSlice(10);
    
    // Nine-slice allows cards to scale while maintaining border proportions
    card.SetSize(new Vector2(2.0f, 3.0f)); // Scale up while preserving borders
    

    Card Highlighting

    Cards can be highlighted for visual feedback:

    // Highlight a card
    card.SetHighlight(true, Color.Yellow, 1.0f);
    
    // Remove highlight
    card.SetHighlight(false, Color.Blank);
    
    // Pulse highlight effect
    card.SetHighlight(true, Color.Gold, Mathf.Sin(time * 5.0f) * 0.5f + 0.5f);
    

    Drag and Drop System

    Implement card drag and drop with mouse interaction:

    private Card3D? currentCard;
    private bool isDragging;
    private Vector3 dragOffset;
    
    private void HandleMouseInput()
    {
        Vector2 mousePos = Raylib.GetMousePosition();
        
        // Start dragging
        if (Raylib.IsMouseButtonPressed(MouseButton.Left))
        {
            foreach (var card in playerHand)
            {
                if (card.IsPointOver(mousePos, camera, out string section))
                {
                    currentCard = card;
                    isDragging = true;
                    
                    // Calculate drag offset
                    Vector3 cardScreenPos = RaycastHelper.WorldToScreen(
                        currentCard.GetPosition(), camera);
                    dragOffset = new Vector3(
                        mousePos.X - cardScreenPos.X, 
                        mousePos.Y - cardScreenPos.Y, 
                        0);
                    break;
                }
            }
        }
        
        // Update drag position
        if (isDragging && currentCard != null)
        {
            Ray ray = Raylib.GetScreenToWorldRay(mousePos, camera);
            Vector3 planeNormal = Vector3.UnitY;
            Vector3 planePoint = new Vector3(0, 0.5f, 0);
            
            float denominator = Vector3.Dot(ray.Direction, planeNormal);
            if (Math.Abs(denominator) > 0.0001f)
            {
                float t = Vector3.Dot(planePoint - ray.Position, planeNormal) / denominator;
                if (t >= 0)
                {
                    Vector3 intersectionPoint = ray.Position + ray.Direction * t;
                    currentCard.SetPosition(new Vector3(
                        intersectionPoint.X, 0.5f, intersectionPoint.Z));
                }
            }
        }
        
        // End dragging
        if (Raylib.IsMouseButtonReleased(MouseButton.Left) && isDragging)
        {
            isDragging = false;
            
            // Check drop targets
            if (IsDroppedOnDiscardPile())
            {
                DiscardCard(currentCard);
            }
            else
            {
                ReturnToHand(currentCard);
            }
            
            currentCard = null;
        }
    }
    

    Card Animations

    Cards support smooth movement and rotation animations:

    // Animate card to a position
    card.MoveTo(targetPosition, duration: 0.5f);
    
    // Animate card rotation
    card.RotateTo(targetRotation, duration: 0.3f);
    
    // Flip animation
    card.FlipCard(duration: 0.2f);
    
    // Custom animation with easing
    card.AnimateProperty(
        property: "position",
        targetValue: targetPosition,
        duration: 1.0f,
        easing: EasingType.EaseOutBounce
    );
    

    Creating a Complete Card Game

    Here's a complete example of setting up a card game scene:

    public class CardGameScene : Scene
    {
        private Camera3D camera;
        private CardPile3D drawPile;
        private CardPile3D discardPile;
        private List<Card3D> playerHand;
        private Card3D? currentCard;
        private bool isDragging;
        
        public override void Load()
        {
            // Initialize camera
            camera = new Camera3D
            {
                Position = new Vector3(0, 10, 0),
                Target = new Vector3(0, 0, 0),
                Up = new Vector3(0, 0, -1),
                FovY = 45,
                Projection = CameraProjection.Perspective
            };
            
            // Create card piles
            drawPile = new CardPile3D(new Vector3(-4, 0.2f, -2.0f), Vector3.Zero, 0.01f);
            discardPile = new CardPile3D(new Vector3(4, 0.2f, -2.0f), Vector3.Zero, 0.01f);
            playerHand = new List<Card3D>();
            
            // Create and shuffle deck
            CreateDeck();
            drawPile.Shuffle();
            
            // Draw initial hand
            for (int i = 0; i < 5; i++)
            {
                DrawCard();
            }
        }
        
        private void CreateDeck()
        {
            foreach (CardSuit suit in Enum.GetValues(typeof(CardSuit)))
            {
                foreach (CardValue value in Enum.GetValues(typeof(CardValue)))
                {
                    var cardData = new CardData(suit, value);
                    var frontTexture = GenerateCardTexture(suit, value);
                    var backTexture = LoadCardBackTexture();
                    
                    var card = new Card3D(
                        frontTexture, backTexture, 
                        Vector3.Zero, 
                        new Vector2(1.5f, 2.0f), 
                        0.05f);
                    
                    // Add interactive sections
                    card.AddSection("title", new Rectangle(0.1f, 0.05f, 0.8f, 0.15f));
                    card.AddSection("artwork", new Rectangle(0.15f, 0.25f, 0.7f, 0.4f));
                    card.AddSection("stats", new Rectangle(0.1f, 0.75f, 0.8f, 0.2f));
                    
                    card.SetFaceUp(false);
                    drawPile.AddCard(card, false);
                }
            }
        }
        
        private void DrawCard()
        {
            Card3D? card = drawPile.DrawCard(true);
            if (card != null)
            {
                playerHand.Add(card);
                
                // Position in hand
                int cardIndex = playerHand.Count - 1;
                float xPos = -2.0f + cardIndex * 0.8f;
                card.SetPosition(new Vector3(xPos, 0.5f, 2.0f));
                card.SetFaceUp(true);
            }
        }
        
        public override void Update(double deltaTime)
        {
            float dt = (float)deltaTime;
            
            // Update card piles
            drawPile.Update(dt);
            discardPile.Update(dt);
            
            // Update player hand
            foreach (var card in playerHand)
            {
                card.Update(dt);
            }
            
            // Handle input
            HandleMouseInput();
        }
        
        public override void Render(int width, int height)
        {
            Raylib.ClearBackground(Color.DarkGreen);
            
            Raylib.BeginMode3D(camera);
            {
                // Draw table/background
                Raylib.DrawCube(Vector3.Zero, 10.0f, 0.1f, 6.0f, Color.Green);
                
                // Draw card piles
                drawPile.Draw(camera);
                discardPile.Draw(camera);
                
                // Draw player hand
                foreach (var card in playerHand)
                {
                    card.Draw(camera);
                }
            }
            Raylib.EndMode3D();
            
            // Draw UI
            DrawUI();
        }
        
        private void DrawUI()
        {
            Raylib.DrawText($"Cards: {drawPile.GetCardCount()}", 20, 20, 20, Color.White);
            Raylib.DrawText("Click draw pile for cards", 20, 50, 16, Color.LightGray);
            Raylib.DrawText("Drag cards to discard", 20, 70, 16, Color.LightGray);
        }
    }
    

    PGPSCN Integration

    Define cards in PGPSCN scene files for data-driven card games:

    {
      "version": "1.0",
      "sceneType": "3d",
      "assets": {
        "textures": {
          "card_back": "assets/cards/card_back.png",
          "card_hearts": "assets/cards/hearts.png",
          "card_spades": "assets/cards/spades.png"
        }
      },
      "ui_elements": [
        {
          "id": "draw_pile",
          "type": "CardPile3D",
          "properties": {
            "position": {"x": -4, "y": 0.2, "z": -2},
            "rotation": {"x": 0, "y": 0, "z": 0},
            "stackOffset": 0.01,
            "maxVisible": 1
          }
        },
        {
          "id": "discard_pile", 
          "type": "CardPile3D",
          "properties": {
            "position": {"x": 4, "y": 0.2, "z": -2},
            "rotation": {"x": 0, "y": 0, "z": 0},
            "stackOffset": 0.01,
            "maxVisible": 3
          }
        },
        {
          "id": "player_card_1",
          "type": "Card3D",
          "properties": {
            "frontTexture": "card_hearts",
            "backTexture": "card_back",
            "position": {"x": -2, "y": 0.5, "z": 2},
            "size": {"width": 1.5, "height": 2.0},
            "thickness": 0.05,
            "faceUp": true
          },
          "sections": [
            {
              "name": "title",
              "rect": {"x": 0.1, "y": 0.05, "width": 0.8, "height": 0.15},
              "tooltip": "Card title and cost"
            },
            {
              "name": "artwork",
              "rect": {"x": 0.15, "y": 0.25, "width": 0.7, "height": 0.4},
              "tooltip": "Main card artwork"
            }
          ],
          "actions": [
            {
              "trigger": "onClick",
              "section": "title",
              "command": {
                "type": "scene_method",
                "method": "ShowCardDetails"
              }
            }
          ]
        }
      ]
    }
    

    Texture Generation

    The engine includes utilities for generating card textures programmatically:

    using PopcorningGuineaPig.Engine;
    
    // Generate card back texture
    Texture2D cardBack = TextureGenerator.GenerateCardBackTexture(
        width: 200, 
        height: 300, 
        color: new Color(0, 0, 128, 255)
    );
    
    // Generate suit-specific card front
    Texture2D cardFront = TextureGenerator.GenerateCardFrontTexture(
        width: 200,
        height: 300,
        suit: CardSuit.Hearts,
        value: CardValue.Ace
    );
    
    // Generate table texture
    Texture2D tableTexture = TextureGenerator.GenerateTableTexture(1024, 1024);
    

    Best Practices

    Performance Optimization

    1. Texture Management: Reuse textures across similar cards
    2. Update Batching: Only update cards that are moving or changing
    3. Culling: Don't render cards outside the camera view
    4. LOD: Use lower detail models for distant cards
    // Efficient texture reuse
    private Dictionary<string, Texture2D> cardTextures = new();
    
    private Texture2D GetCardTexture(CardSuit suit, CardValue value)
    {
        string key = $"{suit}_{value}";
        if (!cardTextures.ContainsKey(key))
        {
            cardTextures[key] = TextureGenerator.GenerateCardFrontTexture(
                200, 300, suit, value);
        }
        return cardTextures[key];
    }
    
    // Selective updates
    foreach (var card in activeCards.Where(c => c.IsMoving || c.IsDirty))
    {
        card.Update(deltaTime);
    }
    

    Memory Management

    public override void Unload()
    {
        // Clean up card textures
        foreach (var texture in cardTextures.Values)
        {
            Raylib.UnloadTexture(texture);
        }
        cardTextures.Clear();
        
        // Unload card piles
        drawPile.Unload();
        discardPile.Unload();
        
        // Clean up individual cards
        foreach (var card in playerHand)
        {
            card.Unload();
        }
    }
    

    Game State Management

    [Serializable]
    public class GameState
    {
        public List<CardData> DrawPile { get; set; }
        public List<CardData> DiscardPile { get; set; }
        public List<CardData> PlayerHand { get; set; }
        public int Score { get; set; }
    }
    
    public void SaveGame()
    {
        var state = new GameState
        {
            DrawPile = drawPile.GetCards().Select(c => c.Data).ToList(),
            DiscardPile = discardPile.GetCards().Select(c => c.Data).ToList(),
            PlayerHand = playerHand.Select(c => c.Data).ToList(),
            Score = currentScore
        };
        
        File.WriteAllText("savegame.json", JsonSerializer.Serialize(state));
    }
    

    Visual Effects

    Particle Effects

    // Card draw effect
    var drawEffect = new ParticleEffect
    {
        Position = drawPile.GetPosition(),
        Color = Color.Gold,
        ParticleCount = 10,
        Duration = 0.5f,
        Spread = 0.2f
    };
    drawEffect.Trigger();
    
    // Card discard effect
    var discardEffect = new ParticleEffect
    {
        Position = card.GetPosition(),
        Color = Color.Red,
        ParticleCount = 15,
        Duration = 0.8f,
        Velocity = new Vector3(0, 2, 0)
    };
    discardEffect.Trigger();
    

    Card Trails

    // Add trail effect to moving cards
    public class CardTrail
    {
        private List<Vector3> positions = new();
        private float maxLength = 10;
        
        public void Update(Vector3 currentPosition)
        {
            positions.Add(currentPosition);
            if (positions.Count > maxLength)
            {
                positions.RemoveAt(0);
            }
        }
        
        public void Draw()
        {
            for (int i = 1; i < positions.Count; i++)
            {
                float alpha = (float)i / positions.Count;
                Color trailColor = new Color(255, 255, 255, (int)(alpha * 100));
                Raylib.DrawLine3D(positions[i-1], positions[i], trailColor);
            }
        }
    }
    

    Advanced Features

    Card Stacking and Layering

    public class CardLayer
    {
        public List<Card3D> Cards { get; } = new();
        public int Depth { get; set; }
        public bool Interactive { get; set; } = true;
        
        public void AddCard(Card3D card, int index = -1)
        {
            if (index == -1)
                Cards.Add(card);
            else
                Cards.Insert(index, card);
                
            UpdateCardDepths();
        }
        
        private void UpdateCardDepths()
        {
            for (int i = 0; i < Cards.Count; i++)
            {
                var pos = Cards[i].GetPosition();
                Cards[i].SetPosition(new Vector3(pos.X, pos.Y + i * 0.01f, pos.Z));
            }
        }
    }
    

    Custom Card Types

    public class SpellCard : Card3D
    {
        public int ManaCost { get; set; }
        public string SpellEffect { get; set; }
        
        public SpellCard(Texture2D front, Texture2D back, Vector3 position, 
                         int manaCost, string effect) 
            : base(front, back, position, new Vector2(1.5f, 2.0f), 0.05f)
        {
            ManaCost = manaCost;
            SpellEffect = effect;
            
            // Add spell-specific sections
            AddSection("mana_cost", new Rectangle(0.8f, 0.05f, 0.15f, 0.15f));
            AddSection("spell_text", new Rectangle(0.1f, 0.7f, 0.8f, 0.25f));
        }
        
        public override void OnSectionClicked(string sectionName)
        {
            base.OnSectionClicked(sectionName);
            
            if (sectionName == "mana_cost")
            {
                ShowManaCostTooltip();
            }
            else if (sectionName == "spell_text")
            {
                ShowSpellEffectDetails();
            }
        }
    }
    

    The card system in Popcorning Guinea Pig provides a robust foundation for creating engaging card-based games with rich 3D interactions, smooth animations, and flexible customization options.

    Further Reading

    • 3D Scenes - 3D rendering and camera systems
    • PGPSCN Format - Declarative scene definition
    • UI Components - Base UI system
    • Asset Registry - Asset management
    • Edit this page
    In this article
    Back to top © 2024 Popcorning Guinea Pig Game Engine. A custom C# game engine for indie developers.